@elizaos/plugin-inbox 2.0.3-beta.5 → 2.0.3-beta.7

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.
Files changed (119) hide show
  1. package/dist/actions/inbox.d.ts +69 -0
  2. package/dist/actions/inbox.d.ts.map +1 -0
  3. package/dist/actions/inbox.js +345 -0
  4. package/dist/actions/inbox.js.map +1 -0
  5. package/dist/components/inbox/InboxSpatialView.d.ts +54 -0
  6. package/dist/components/inbox/InboxSpatialView.d.ts.map +1 -0
  7. package/dist/components/inbox/InboxSpatialView.js +171 -0
  8. package/dist/components/inbox/InboxSpatialView.js.map +1 -0
  9. package/dist/components/inbox/InboxView.d.ts +64 -0
  10. package/dist/components/inbox/InboxView.d.ts.map +1 -0
  11. package/dist/components/inbox/InboxView.js +169 -0
  12. package/dist/components/inbox/InboxView.js.map +1 -0
  13. package/dist/components/inbox/inbox-view-bundle.d.ts +2 -0
  14. package/dist/components/inbox/inbox-view-bundle.d.ts.map +1 -0
  15. package/dist/components/inbox/inbox-view-bundle.js +5 -0
  16. package/dist/components/inbox/inbox-view-bundle.js.map +1 -0
  17. package/dist/db/index.d.ts +3 -0
  18. package/dist/db/index.d.ts.map +1 -0
  19. package/dist/db/index.js +3 -0
  20. package/dist/db/index.js.map +1 -0
  21. package/dist/db/schema.d.ts +1729 -0
  22. package/dist/db/schema.d.ts.map +1 -0
  23. package/dist/db/schema.js +79 -0
  24. package/dist/db/schema.js.map +1 -0
  25. package/dist/db/sql.d.ts +32 -0
  26. package/dist/db/sql.d.ts.map +1 -0
  27. package/dist/db/sql.js +130 -0
  28. package/dist/db/sql.js.map +1 -0
  29. package/dist/inbox/channel-deep-links.d.ts +7 -0
  30. package/dist/inbox/channel-deep-links.d.ts.map +1 -0
  31. package/dist/inbox/channel-deep-links.js +97 -0
  32. package/dist/inbox/channel-deep-links.js.map +1 -0
  33. package/dist/inbox/config.d.ts +7 -0
  34. package/dist/inbox/config.d.ts.map +1 -0
  35. package/dist/inbox/config.js +61 -0
  36. package/dist/inbox/config.js.map +1 -0
  37. package/dist/inbox/email-curation.d.ts +174 -0
  38. package/dist/inbox/email-curation.d.ts.map +1 -0
  39. package/dist/inbox/email-curation.js +1056 -0
  40. package/dist/inbox/email-curation.js.map +1 -0
  41. package/dist/inbox/email-unsubscribe-types.d.ts +71 -0
  42. package/dist/inbox/email-unsubscribe-types.d.ts.map +1 -0
  43. package/dist/inbox/email-unsubscribe-types.js +1 -0
  44. package/dist/inbox/email-unsubscribe-types.js.map +1 -0
  45. package/dist/inbox/gmail-normalize.d.ts +99 -0
  46. package/dist/inbox/gmail-normalize.d.ts.map +1 -0
  47. package/dist/inbox/gmail-normalize.js +937 -0
  48. package/dist/inbox/gmail-normalize.js.map +1 -0
  49. package/dist/inbox/google-gmail-seam.d.ts +52 -0
  50. package/dist/inbox/google-gmail-seam.d.ts.map +1 -0
  51. package/dist/inbox/google-gmail-seam.js +263 -0
  52. package/dist/inbox/google-gmail-seam.js.map +1 -0
  53. package/dist/inbox/message-fetcher.d.ts +47 -0
  54. package/dist/inbox/message-fetcher.d.ts.map +1 -0
  55. package/dist/inbox/message-fetcher.js +461 -0
  56. package/dist/inbox/message-fetcher.js.map +1 -0
  57. package/dist/inbox/migration.d.ts +46 -0
  58. package/dist/inbox/migration.d.ts.map +1 -0
  59. package/dist/inbox/migration.js +114 -0
  60. package/dist/inbox/migration.js.map +1 -0
  61. package/dist/inbox/reflection.d.ts +40 -0
  62. package/dist/inbox/reflection.d.ts.map +1 -0
  63. package/dist/inbox/reflection.js +142 -0
  64. package/dist/inbox/reflection.js.map +1 -0
  65. package/dist/inbox/repository.d.ts +58 -0
  66. package/dist/inbox/repository.d.ts.map +1 -0
  67. package/dist/inbox/repository.js +376 -0
  68. package/dist/inbox/repository.js.map +1 -0
  69. package/dist/inbox/service.d.ts +149 -0
  70. package/dist/inbox/service.d.ts.map +1 -0
  71. package/dist/inbox/service.js +247 -0
  72. package/dist/inbox/service.js.map +1 -0
  73. package/dist/inbox/triage-classifier.d.ts +28 -0
  74. package/dist/inbox/triage-classifier.d.ts.map +1 -0
  75. package/dist/inbox/triage-classifier.js +306 -0
  76. package/dist/inbox/triage-classifier.js.map +1 -0
  77. package/dist/inbox/types.d.ts +124 -0
  78. package/dist/inbox/types.d.ts.map +1 -0
  79. package/dist/inbox/types.js +1 -0
  80. package/dist/inbox/types.js.map +1 -0
  81. package/dist/inbox/unsubscribe-repository.d.ts +14 -0
  82. package/dist/inbox/unsubscribe-repository.d.ts.map +1 -0
  83. package/dist/inbox/unsubscribe-repository.js +112 -0
  84. package/dist/inbox/unsubscribe-repository.js.map +1 -0
  85. package/dist/inbox/unsubscribe-service.d.ts +41 -0
  86. package/dist/inbox/unsubscribe-service.d.ts.map +1 -0
  87. package/dist/inbox/unsubscribe-service.js +351 -0
  88. package/dist/inbox/unsubscribe-service.js.map +1 -0
  89. package/dist/index.d.ts +20 -0
  90. package/dist/index.d.ts.map +1 -0
  91. package/dist/index.js +70 -0
  92. package/dist/index.js.map +1 -0
  93. package/dist/plugin.d.ts +4 -0
  94. package/dist/plugin.d.ts.map +1 -0
  95. package/dist/plugin.js +38 -0
  96. package/dist/plugin.js.map +1 -0
  97. package/dist/providers/cross-channel-context.d.ts +21 -0
  98. package/dist/providers/cross-channel-context.d.ts.map +1 -0
  99. package/dist/providers/cross-channel-context.js +96 -0
  100. package/dist/providers/cross-channel-context.js.map +1 -0
  101. package/dist/providers/inbox-triage.d.ts +12 -0
  102. package/dist/providers/inbox-triage.d.ts.map +1 -0
  103. package/dist/providers/inbox-triage.js +98 -0
  104. package/dist/providers/inbox-triage.js.map +1 -0
  105. package/dist/register-terminal-view.d.ts +15 -0
  106. package/dist/register-terminal-view.d.ts.map +1 -0
  107. package/dist/register-terminal-view.js +21 -0
  108. package/dist/register-terminal-view.js.map +1 -0
  109. package/dist/register.d.ts +9 -0
  110. package/dist/register.d.ts.map +1 -0
  111. package/dist/register.js +5 -0
  112. package/dist/register.js.map +1 -0
  113. package/dist/types.d.ts +42 -0
  114. package/dist/types.d.ts.map +1 -0
  115. package/dist/types.js +25 -0
  116. package/dist/types.js.map +1 -0
  117. package/dist/views/bundle.js +315 -0
  118. package/dist/views/bundle.js.map +1 -0
  119. package/package.json +9 -9
@@ -0,0 +1,171 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import {
3
+ Button,
4
+ Card,
5
+ Divider,
6
+ HStack,
7
+ List,
8
+ Text,
9
+ VStack
10
+ } from "@elizaos/ui/spatial";
11
+ import {
12
+ INBOX_CHANNEL_LABELS,
13
+ INBOX_CHANNELS
14
+ } from "../../types.js";
15
+ const DEFAULT_FILTERS = INBOX_CHANNELS.map((channel) => ({
16
+ channel,
17
+ label: INBOX_CHANNEL_LABELS[channel],
18
+ active: false
19
+ }));
20
+ const EMPTY_INBOX_SNAPSHOT = {
21
+ status: "loading",
22
+ items: [],
23
+ filters: DEFAULT_FILTERS,
24
+ activeFilterCount: 0,
25
+ hasConnectedChannels: false,
26
+ nudge: null,
27
+ error: null
28
+ };
29
+ function groupByChannel(items) {
30
+ const groups = [];
31
+ for (const channel of INBOX_CHANNELS) {
32
+ const channelItems = items.filter((item) => item.channel === channel);
33
+ if (channelItems.length === 0) continue;
34
+ groups.push({
35
+ channel,
36
+ label: INBOX_CHANNEL_LABELS[channel],
37
+ items: channelItems
38
+ });
39
+ }
40
+ return groups;
41
+ }
42
+ function formatTime(value) {
43
+ const date = new Date(value);
44
+ if (Number.isNaN(date.getTime())) return value;
45
+ return date.toLocaleString(void 0, {
46
+ month: "short",
47
+ day: "numeric",
48
+ hour: "numeric",
49
+ minute: "2-digit"
50
+ });
51
+ }
52
+ function InboxSpatialView({
53
+ snapshot,
54
+ onAction
55
+ }) {
56
+ const dispatch = (action) => () => onAction?.(action);
57
+ return /* @__PURE__ */ jsxs(Card, { gap: 1, padding: 1, children: [
58
+ /* @__PURE__ */ jsx(InboxChannelFilters, { filters: snapshot.filters, dispatch }),
59
+ /* @__PURE__ */ jsx(InboxBody, { snapshot, dispatch })
60
+ ] });
61
+ }
62
+ function InboxChannelFilters({
63
+ filters,
64
+ dispatch
65
+ }) {
66
+ return /* @__PURE__ */ jsx(HStack, { gap: 1, wrap: true, align: "center", children: filters.map((filter) => /* @__PURE__ */ jsx(
67
+ Button,
68
+ {
69
+ variant: filter.active ? "solid" : "outline",
70
+ tone: filter.active ? "primary" : "default",
71
+ agent: `inbox-channel-${filter.channel}`,
72
+ onPress: dispatch(`channel:${filter.channel}`),
73
+ children: filter.active ? `* ${filter.label}` : filter.label
74
+ },
75
+ filter.channel
76
+ )) });
77
+ }
78
+ function InboxBody({
79
+ snapshot,
80
+ dispatch
81
+ }) {
82
+ if (snapshot.status === "loading") {
83
+ return /* @__PURE__ */ jsx(Text, { tone: "muted", align: "center", style: "caption", children: "Loading inbox" });
84
+ }
85
+ if (snapshot.status === "error") {
86
+ return /* @__PURE__ */ jsxs(VStack, { gap: 1, children: [
87
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Couldn't load inbox" }),
88
+ /* @__PURE__ */ jsx(Text, { tone: "danger", style: "caption", children: snapshot.error ?? "Could not load inbox." }),
89
+ /* @__PURE__ */ jsx(Button, { width: "100%", agent: "retry", onPress: dispatch("retry"), children: "Retry" })
90
+ ] });
91
+ }
92
+ if (snapshot.status === "empty" || snapshot.items.length === 0) {
93
+ return /* @__PURE__ */ jsx(InboxEmptyBody, { snapshot, dispatch });
94
+ }
95
+ return /* @__PURE__ */ jsx(InboxReadyBody, { snapshot, dispatch });
96
+ }
97
+ function InboxEmptyBody({
98
+ snapshot,
99
+ dispatch
100
+ }) {
101
+ const noChannels = !snapshot.hasConnectedChannels && snapshot.activeFilterCount === 0;
102
+ if (noChannels) {
103
+ return /* @__PURE__ */ jsxs(VStack, { gap: 1, children: [
104
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "None" }),
105
+ /* @__PURE__ */ jsx(Button, { width: "100%", agent: "connect", onPress: dispatch("connect"), children: "Connect" })
106
+ ] });
107
+ }
108
+ return /* @__PURE__ */ jsx(VStack, { gap: 1, children: /* @__PURE__ */ jsx(Text, { bold: true, children: "Inbox zero" }) });
109
+ }
110
+ function InboxReadyBody({
111
+ snapshot,
112
+ dispatch
113
+ }) {
114
+ const groups = groupByChannel(snapshot.items);
115
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
116
+ snapshot.nudge ? /* @__PURE__ */ jsx(Text, { tone: "muted", style: "caption", children: snapshot.nudge }) : null,
117
+ groups.map((group) => /* @__PURE__ */ jsx(
118
+ InboxChannelGroupBody,
119
+ {
120
+ group,
121
+ dispatch
122
+ },
123
+ group.channel
124
+ ))
125
+ ] });
126
+ }
127
+ function InboxChannelGroupBody({
128
+ group,
129
+ dispatch
130
+ }) {
131
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
132
+ /* @__PURE__ */ jsx(Divider, { label: `${group.label} (${group.items.length})` }),
133
+ /* @__PURE__ */ jsx(List, { gap: 1, children: group.items.slice(0, 12).map((item) => {
134
+ const title = item.subject ?? item.sender;
135
+ const meta = item.preview ? `${item.sender} - ${item.preview}` : item.sender;
136
+ return (
137
+ // The title gets its own full-width line so a long subject is never
138
+ // truncated by a sibling control; the sender/preview meta and the
139
+ // Open action share the second line under the unread dot.
140
+ /* @__PURE__ */ jsxs(VStack, { gap: 0, children: [
141
+ /* @__PURE__ */ jsxs(HStack, { gap: 1, align: "center", children: [
142
+ /* @__PURE__ */ jsx(Text, { tone: "primary", wrap: false, children: item.unread ? "\u25CF" : "\u25CB" }),
143
+ /* @__PURE__ */ jsx(Text, { bold: true, grow: 1, children: title })
144
+ ] }),
145
+ /* @__PURE__ */ jsxs(HStack, { gap: 1, align: "center", children: [
146
+ /* @__PURE__ */ jsxs(Text, { style: "caption", tone: "muted", grow: 1, wrap: false, children: [
147
+ meta,
148
+ " \u2022 ",
149
+ formatTime(item.receivedAt)
150
+ ] }),
151
+ /* @__PURE__ */ jsx(
152
+ Button,
153
+ {
154
+ variant: "outline",
155
+ tone: "default",
156
+ agent: `open:${item.id}`,
157
+ onPress: dispatch(`open:${item.id}`),
158
+ children: "Open"
159
+ }
160
+ )
161
+ ] })
162
+ ] }, item.id)
163
+ );
164
+ }) })
165
+ ] });
166
+ }
167
+ export {
168
+ EMPTY_INBOX_SNAPSHOT,
169
+ InboxSpatialView
170
+ };
171
+ //# sourceMappingURL=InboxSpatialView.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/inbox/InboxSpatialView.tsx"],"sourcesContent":["/**\n * InboxSpatialView — the cross-channel inbox authored once with the spatial\n * vocabulary so it renders correctly wherever it is displayed:\n *\n * - GUI / XR — mounted in `<SpatialSurface>` (DOM; XR scales up).\n * - TUI — rendered to real terminal lines by the agent terminal, via\n * `registerSpatialTerminalView` (see `register-terminal-view.tsx`).\n *\n * It is purely presentational (a snapshot + an action callback in, primitives\n * out) and imports ONLY the cross-modality primitives plus the view's local\n * display types, so it is safe to render in the Node agent process where the\n * terminal lives (no `@elizaos/ui` renderer barrel, no fetch).\n *\n * The live data wrapper {@link InboxView} owns the `/api/lifeops/inbox` fetch,\n * the background poll, and the channel-filter state; it builds an\n * {@link InboxSnapshot} and dispatches user intent back through `onAction`.\n */\n\nimport {\n Button,\n Card,\n Divider,\n HStack,\n List,\n Text,\n VStack,\n} from \"@elizaos/ui/spatial\";\nimport {\n INBOX_CHANNEL_LABELS,\n INBOX_CHANNELS,\n type InboxChannel,\n type InboxItem,\n} from \"../../types.js\";\n\n/** Which fetch state the inbox surface is currently in. */\nexport type InboxStatus = \"loading\" | \"error\" | \"empty\" | \"ready\";\n\n/** One channel chip's display state in the filter row. */\nexport interface InboxChannelFilter {\n channel: InboxChannel;\n label: string;\n active: boolean;\n}\n\nexport interface InboxSnapshot {\n /** Current fetch state. */\n status: InboxStatus;\n /** Triage items (already filtered to the active channel selection). */\n items: InboxItem[];\n /** Channel filter chips in display order. */\n filters: InboxChannelFilter[];\n /** Number of active channel filters (drives the empty-state copy). */\n activeFilterCount: number;\n /** True when at least one channel reported messages in the payload. */\n hasConnectedChannels: boolean;\n /** Proactive one-liner (\"N threads still need a reply\"); absent when zero. */\n nudge?: string | null;\n /** Error text for the error state. */\n error?: string | null;\n}\n\nconst DEFAULT_FILTERS: InboxChannelFilter[] = INBOX_CHANNELS.map((channel) => ({\n channel,\n label: INBOX_CHANNEL_LABELS[channel],\n active: false,\n}));\n\n/** A snapshot every surface can render before live data arrives. */\nexport const EMPTY_INBOX_SNAPSHOT: InboxSnapshot = {\n status: \"loading\",\n items: [],\n filters: DEFAULT_FILTERS,\n activeFilterCount: 0,\n hasConnectedChannels: false,\n nudge: null,\n error: null,\n};\n\ninterface InboxChannelGroup {\n channel: InboxChannel;\n label: string;\n items: InboxItem[];\n}\n\n/** Group items by channel in display order; only channels with items appear. */\nfunction groupByChannel(items: InboxItem[]): InboxChannelGroup[] {\n const groups: InboxChannelGroup[] = [];\n for (const channel of INBOX_CHANNELS) {\n const channelItems = items.filter((item) => item.channel === channel);\n if (channelItems.length === 0) continue;\n groups.push({\n channel,\n label: INBOX_CHANNEL_LABELS[channel],\n items: channelItems,\n });\n }\n return groups;\n}\n\nfunction formatTime(value: string): string {\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return value;\n return date.toLocaleString(undefined, {\n month: \"short\",\n day: \"numeric\",\n hour: \"numeric\",\n minute: \"2-digit\",\n });\n}\n\nexport interface InboxSpatialViewProps {\n snapshot: InboxSnapshot;\n /**\n * Dispatch by agent id: `retry`, `connect`, `channel:<id>` (toggle a channel\n * filter), and `open:<messageId>` (open a triage item).\n */\n onAction?: (action: string) => void;\n}\n\nexport function InboxSpatialView({\n snapshot,\n onAction,\n}: InboxSpatialViewProps) {\n const dispatch = (action: string) => () => onAction?.(action);\n\n return (\n <Card gap={1} padding={1}>\n <InboxChannelFilters filters={snapshot.filters} dispatch={dispatch} />\n <InboxBody snapshot={snapshot} dispatch={dispatch} />\n </Card>\n );\n}\n\nfunction InboxChannelFilters({\n filters,\n dispatch,\n}: {\n filters: InboxChannelFilter[];\n dispatch: (action: string) => () => void;\n}) {\n return (\n <HStack gap={1} wrap align=\"center\">\n {filters.map((filter) => (\n <Button\n key={filter.channel}\n variant={filter.active ? \"solid\" : \"outline\"}\n tone={filter.active ? \"primary\" : \"default\"}\n agent={`inbox-channel-${filter.channel}`}\n onPress={dispatch(`channel:${filter.channel}`)}\n >\n {filter.active ? `* ${filter.label}` : filter.label}\n </Button>\n ))}\n </HStack>\n );\n}\n\nfunction InboxBody({\n snapshot,\n dispatch,\n}: {\n snapshot: InboxSnapshot;\n dispatch: (action: string) => () => void;\n}) {\n if (snapshot.status === \"loading\") {\n return (\n <Text tone=\"muted\" align=\"center\" style=\"caption\">\n Loading inbox\n </Text>\n );\n }\n\n if (snapshot.status === \"error\") {\n return (\n <VStack gap={1}>\n <Text bold>Couldn't load inbox</Text>\n <Text tone=\"danger\" style=\"caption\">\n {snapshot.error ?? \"Could not load inbox.\"}\n </Text>\n <Button width=\"100%\" agent=\"retry\" onPress={dispatch(\"retry\")}>\n Retry\n </Button>\n </VStack>\n );\n }\n\n if (snapshot.status === \"empty\" || snapshot.items.length === 0) {\n return <InboxEmptyBody snapshot={snapshot} dispatch={dispatch} />;\n }\n\n return <InboxReadyBody snapshot={snapshot} dispatch={dispatch} />;\n}\n\nfunction InboxEmptyBody({\n snapshot,\n dispatch,\n}: {\n snapshot: InboxSnapshot;\n dispatch: (action: string) => () => void;\n}) {\n const noChannels =\n !snapshot.hasConnectedChannels && snapshot.activeFilterCount === 0;\n if (noChannels) {\n return (\n <VStack gap={1}>\n <Text bold>None</Text>\n <Button width=\"100%\" agent=\"connect\" onPress={dispatch(\"connect\")}>\n Connect\n </Button>\n </VStack>\n );\n }\n return (\n <VStack gap={1}>\n <Text bold>Inbox zero</Text>\n </VStack>\n );\n}\n\nfunction InboxReadyBody({\n snapshot,\n dispatch,\n}: {\n snapshot: InboxSnapshot;\n dispatch: (action: string) => () => void;\n}) {\n const groups = groupByChannel(snapshot.items);\n return (\n <>\n {snapshot.nudge ? (\n <Text tone=\"muted\" style=\"caption\">\n {snapshot.nudge}\n </Text>\n ) : null}\n {groups.map((group) => (\n <InboxChannelGroupBody\n key={group.channel}\n group={group}\n dispatch={dispatch}\n />\n ))}\n </>\n );\n}\n\nfunction InboxChannelGroupBody({\n group,\n dispatch,\n}: {\n group: InboxChannelGroup;\n dispatch: (action: string) => () => void;\n}) {\n return (\n <>\n <Divider label={`${group.label} (${group.items.length})`} />\n <List gap={1}>\n {group.items.slice(0, 12).map((item) => {\n const title = item.subject ?? item.sender;\n const meta = item.preview\n ? `${item.sender} - ${item.preview}`\n : item.sender;\n return (\n // The title gets its own full-width line so a long subject is never\n // truncated by a sibling control; the sender/preview meta and the\n // Open action share the second line under the unread dot.\n <VStack key={item.id} gap={0}>\n <HStack gap={1} align=\"center\">\n <Text tone=\"primary\" wrap={false}>\n {item.unread ? \"●\" : \"○\"}\n </Text>\n <Text bold grow={1}>\n {title}\n </Text>\n </HStack>\n <HStack gap={1} align=\"center\">\n <Text style=\"caption\" tone=\"muted\" grow={1} wrap={false}>\n {meta} • {formatTime(item.receivedAt)}\n </Text>\n <Button\n variant=\"outline\"\n tone=\"default\"\n agent={`open:${item.id}`}\n onPress={dispatch(`open:${item.id}`)}\n >\n Open\n </Button>\n </HStack>\n </VStack>\n );\n })}\n </List>\n </>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * InboxView — the single GUI/XR data wrapper for the cross-channel inbox.
3
+ *
4
+ * It owns the live inbox data (the single read-only endpoint served by the
5
+ * personal-assistant routes, the background poll, the channel-filter selection,
6
+ * and the loading/error/empty/ready state machine) and renders the one
7
+ * presentational {@link InboxSpatialView} inside a {@link SpatialSurface}.
8
+ * Omitting the `modality` prop lets `SpatialSurface` auto-detect GUI vs XR via
9
+ * `window.__elizaXRContext`, so the SAME component serves both surfaces. The TUI
10
+ * surface renders the same `InboxSpatialView` through the terminal registry
11
+ * (see `register-terminal-view.tsx`).
12
+ *
13
+ * Data source (PA owns the persistence + connector pulls; this plugin renders):
14
+ * GET {base}/api/lifeops/inbox?channels=
15
+ *
16
+ * The default fetcher builds its URL from `client.getBaseUrl()`; tests inject
17
+ * the fetcher seam so they stay offline. The wire payload is a flat list of
18
+ * messages plus per-channel counts; we map each message to a flat display item
19
+ * at the fetch boundary so the rest of the view renders display-only.
20
+ *
21
+ * This plugin MUST NOT import from @elizaos/plugin-personal-assistant. The wire
22
+ * DTOs below are declared locally to match the JSON shape PA emits
23
+ * (`LifeOpsInbox` / `LifeOpsInboxMessage` in @elizaos/shared).
24
+ */
25
+ import type { ReactNode } from "react";
26
+ import { type InboxChannel } from "../../types.js";
27
+ interface InboxMessageSenderWire {
28
+ id: string;
29
+ displayName: string;
30
+ email: string | null;
31
+ avatarUrl: string | null;
32
+ }
33
+ interface InboxMessageWire {
34
+ id: string;
35
+ channel: string;
36
+ sender: InboxMessageSenderWire;
37
+ subject: string | null;
38
+ snippet: string;
39
+ receivedAt: string;
40
+ unread: boolean;
41
+ threadId?: string;
42
+ }
43
+ interface InboxChannelCountWire {
44
+ total: number;
45
+ unread: number;
46
+ }
47
+ interface InboxWire {
48
+ messages: InboxMessageWire[];
49
+ channelCounts: Record<string, InboxChannelCountWire>;
50
+ fetchedAt: string;
51
+ }
52
+ export interface InboxFetchers {
53
+ /** Fetch the inbox. `channels` narrows the server query when non-empty. */
54
+ fetchInbox: (channels: InboxChannel[]) => Promise<InboxWire>;
55
+ }
56
+ export interface InboxViewProps {
57
+ /** Owner display name. Reserved for host wiring; not currently rendered. */
58
+ ownerName?: string;
59
+ /** Test/host injection seam. Defaults to the real `/api/lifeops/inbox` GET. */
60
+ fetchers?: InboxFetchers;
61
+ }
62
+ export declare function InboxView(props?: InboxViewProps): ReactNode;
63
+ export default InboxView;
64
+ //# sourceMappingURL=InboxView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InboxView.d.ts","sourceRoot":"","sources":["../../../src/components/inbox/InboxView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAGL,KAAK,YAAY,EAElB,MAAM,gBAAgB,CAAC;AAcxB,UAAU,sBAAsB;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,UAAU,gBAAgB;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,sBAAsB,CAAC;IAC/B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,qBAAqB;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,SAAS;IACjB,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC;IACrD,SAAS,EAAE,MAAM,CAAC;CACnB;AAMD,MAAM,WAAW,aAAa;IAC5B,2EAA2E;IAC3E,UAAU,EAAE,CAAC,QAAQ,EAAE,YAAY,EAAE,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC;CAC9D;AAqBD,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,aAAa,CAAC;CAC1B;AA2FD,wBAAgB,SAAS,CAAC,KAAK,GAAE,cAAmB,GAAG,SAAS,CAkI/D;AAED,eAAe,SAAS,CAAC"}
@@ -0,0 +1,169 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { client } from "@elizaos/ui";
3
+ import { SpatialSurface } from "@elizaos/ui/spatial";
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
+ import {
6
+ INBOX_CHANNEL_LABELS,
7
+ INBOX_CHANNELS
8
+ } from "../../types.js";
9
+ import {
10
+ InboxSpatialView
11
+ } from "./InboxSpatialView.js";
12
+ async function getInbox(channels) {
13
+ const params = new URLSearchParams();
14
+ if (channels.length > 0) params.set("channels", channels.join(","));
15
+ const query = params.toString();
16
+ const path = `/api/lifeops/inbox${query ? `?${query}` : ""}`;
17
+ const response = await fetch(`${client.getBaseUrl()}${path}`);
18
+ if (!response.ok) {
19
+ throw new Error(`Inbox request failed (${response.status})`);
20
+ }
21
+ return await response.json();
22
+ }
23
+ const defaultFetchers = {
24
+ fetchInbox: getInbox
25
+ };
26
+ const INBOX_POLL_MS = 2e4;
27
+ const KNOWN_CHANNELS = new Set(INBOX_CHANNELS);
28
+ function isKnownChannel(value) {
29
+ return KNOWN_CHANNELS.has(value);
30
+ }
31
+ function mapMessage(message) {
32
+ if (!isKnownChannel(message.channel)) return null;
33
+ return {
34
+ id: message.id,
35
+ channel: message.channel,
36
+ sender: message.sender.displayName,
37
+ subject: message.subject,
38
+ preview: message.snippet,
39
+ receivedAt: message.receivedAt,
40
+ unread: message.unread,
41
+ threadId: message.threadId ?? null
42
+ };
43
+ }
44
+ function connectedChannels(counts) {
45
+ return INBOX_CHANNELS.filter((channel) => {
46
+ const count = counts[channel];
47
+ return count !== void 0 && count.total > 0;
48
+ });
49
+ }
50
+ function unreadNudge(items) {
51
+ const unread = items.reduce((n, item) => item.unread ? n + 1 : n, 0);
52
+ if (unread === 0) return null;
53
+ return `${unread} thread${unread === 1 ? " still needs" : "s still need"} a reply.`;
54
+ }
55
+ function sendChatPrompt(prompt) {
56
+ client.sendChatMessage?.(
57
+ prompt
58
+ );
59
+ }
60
+ function requestConnect() {
61
+ sendChatPrompt("Connect a messaging channel so you can triage my inbox.");
62
+ }
63
+ function requestOpen(item) {
64
+ if (!item) return;
65
+ const title = item.subject ?? item.sender;
66
+ sendChatPrompt(
67
+ `Open the inbox thread from ${item.sender}${title ? ` \u2014 "${title}"` : ""}.`
68
+ );
69
+ }
70
+ const EMPTY_ITEMS = [];
71
+ function InboxView(props = {}) {
72
+ const fetchers = props.fetchers ?? defaultFetchers;
73
+ const [state, setState] = useState({ kind: "loading" });
74
+ const [activeChannels, setActiveChannels] = useState(
75
+ () => /* @__PURE__ */ new Set()
76
+ );
77
+ const fetchersRef = useRef(fetchers);
78
+ fetchersRef.current = fetchers;
79
+ const load = useCallback((channels, background = false) => {
80
+ let cancelled = false;
81
+ if (!background) setState({ kind: "loading" });
82
+ fetchersRef.current.fetchInbox(channels).then((wire) => {
83
+ if (cancelled) return;
84
+ const items2 = wire.messages.map(mapMessage).filter((item) => item !== null);
85
+ setState({
86
+ kind: "ready",
87
+ data: { items: items2, connected: connectedChannels(wire.channelCounts) }
88
+ });
89
+ }).catch((error) => {
90
+ if (cancelled) return;
91
+ setState({
92
+ kind: "error",
93
+ message: error instanceof Error ? error.message : "Could not load inbox."
94
+ });
95
+ });
96
+ return () => {
97
+ cancelled = true;
98
+ };
99
+ }, []);
100
+ const activeList = useMemo(
101
+ () => INBOX_CHANNELS.filter((channel) => activeChannels.has(channel)),
102
+ [activeChannels]
103
+ );
104
+ useEffect(() => {
105
+ const cancelLoad = load(activeList);
106
+ const timer = setInterval(() => load(activeList, true), INBOX_POLL_MS);
107
+ return () => {
108
+ cancelLoad();
109
+ clearInterval(timer);
110
+ };
111
+ }, [load, activeList]);
112
+ const items = state.kind === "ready" ? state.data.items : EMPTY_ITEMS;
113
+ const onAction = useCallback(
114
+ (action) => {
115
+ if (action.startsWith("channel:")) {
116
+ const channel = action.slice("channel:".length);
117
+ if (!isKnownChannel(channel)) return;
118
+ setActiveChannels((prev) => {
119
+ const next = new Set(prev);
120
+ if (next.has(channel)) next.delete(channel);
121
+ else next.add(channel);
122
+ return next;
123
+ });
124
+ return;
125
+ }
126
+ if (action.startsWith("open:")) {
127
+ const id = action.slice("open:".length);
128
+ requestOpen(items.find((item) => item.id === id));
129
+ return;
130
+ }
131
+ switch (action) {
132
+ case "retry":
133
+ load(activeList);
134
+ return;
135
+ case "connect":
136
+ requestConnect();
137
+ return;
138
+ }
139
+ },
140
+ [items, load, activeList]
141
+ );
142
+ const filters = useMemo(
143
+ () => INBOX_CHANNELS.map((channel) => ({
144
+ channel,
145
+ label: INBOX_CHANNEL_LABELS[channel],
146
+ active: activeChannels.has(channel)
147
+ })),
148
+ [activeChannels]
149
+ );
150
+ const snapshot = useMemo(() => {
151
+ const status = state.kind === "loading" ? "loading" : state.kind === "error" ? "error" : items.length === 0 ? "empty" : "ready";
152
+ return {
153
+ status,
154
+ items,
155
+ filters,
156
+ activeFilterCount: activeChannels.size,
157
+ hasConnectedChannels: state.kind === "ready" && state.data.connected.length > 0,
158
+ nudge: unreadNudge(items),
159
+ error: state.kind === "error" ? state.message : null
160
+ };
161
+ }, [state, items, filters, activeChannels]);
162
+ return /* @__PURE__ */ jsx(SpatialSurface, { children: /* @__PURE__ */ jsx(InboxSpatialView, { snapshot, onAction }) });
163
+ }
164
+ var InboxView_default = InboxView;
165
+ export {
166
+ InboxView,
167
+ InboxView_default as default
168
+ };
169
+ //# sourceMappingURL=InboxView.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/inbox/InboxView.tsx"],"sourcesContent":["/**\n * InboxView — the single GUI/XR data wrapper for the cross-channel inbox.\n *\n * It owns the live inbox data (the single read-only endpoint served by the\n * personal-assistant routes, the background poll, the channel-filter selection,\n * and the loading/error/empty/ready state machine) and renders the one\n * presentational {@link InboxSpatialView} inside a {@link SpatialSurface}.\n * Omitting the `modality` prop lets `SpatialSurface` auto-detect GUI vs XR via\n * `window.__elizaXRContext`, so the SAME component serves both surfaces. The TUI\n * surface renders the same `InboxSpatialView` through the terminal registry\n * (see `register-terminal-view.tsx`).\n *\n * Data source (PA owns the persistence + connector pulls; this plugin renders):\n * GET {base}/api/lifeops/inbox?channels=\n *\n * The default fetcher builds its URL from `client.getBaseUrl()`; tests inject\n * the fetcher seam so they stay offline. The wire payload is a flat list of\n * messages plus per-channel counts; we map each message to a flat display item\n * at the fetch boundary so the rest of the view renders display-only.\n *\n * This plugin MUST NOT import from @elizaos/plugin-personal-assistant. The wire\n * DTOs below are declared locally to match the JSON shape PA emits\n * (`LifeOpsInbox` / `LifeOpsInboxMessage` in @elizaos/shared).\n */\n\nimport { client } from \"@elizaos/ui\";\nimport { SpatialSurface } from \"@elizaos/ui/spatial\";\nimport type { ReactNode } from \"react\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n INBOX_CHANNEL_LABELS,\n INBOX_CHANNELS,\n type InboxChannel,\n type InboxItem,\n} from \"../../types.js\";\nimport {\n type InboxChannelFilter,\n type InboxSnapshot,\n InboxSpatialView,\n type InboxStatus,\n} from \"./InboxSpatialView.js\";\n\n// ---------------------------------------------------------------------------\n// Wire DTOs — local mirror of the JSON shape served by the PA inbox route.\n// Never import PA / @elizaos/shared inbox types here; keep this view's contract\n// self-contained and aligned by shape.\n// ---------------------------------------------------------------------------\n\ninterface InboxMessageSenderWire {\n id: string;\n displayName: string;\n email: string | null;\n avatarUrl: string | null;\n}\n\ninterface InboxMessageWire {\n id: string;\n channel: string;\n sender: InboxMessageSenderWire;\n subject: string | null;\n snippet: string;\n receivedAt: string;\n unread: boolean;\n threadId?: string;\n}\n\ninterface InboxChannelCountWire {\n total: number;\n unread: number;\n}\n\ninterface InboxWire {\n messages: InboxMessageWire[];\n channelCounts: Record<string, InboxChannelCountWire>;\n fetchedAt: string;\n}\n\n// ---------------------------------------------------------------------------\n// Fetcher seam — default to a real GET; tests inject an offline fake.\n// ---------------------------------------------------------------------------\n\nexport interface InboxFetchers {\n /** Fetch the inbox. `channels` narrows the server query when non-empty. */\n fetchInbox: (channels: InboxChannel[]) => Promise<InboxWire>;\n}\n\nasync function getInbox(channels: InboxChannel[]): Promise<InboxWire> {\n const params = new URLSearchParams();\n if (channels.length > 0) params.set(\"channels\", channels.join(\",\"));\n const query = params.toString();\n const path = `/api/lifeops/inbox${query ? `?${query}` : \"\"}`;\n const response = await fetch(`${client.getBaseUrl()}${path}`);\n if (!response.ok) {\n throw new Error(`Inbox request failed (${response.status})`);\n }\n return (await response.json()) as InboxWire;\n}\n\nconst defaultFetchers: InboxFetchers = {\n fetchInbox: getInbox,\n};\n\n/** Background poll cadence — keeps the list fresh without a manual refresh. */\nconst INBOX_POLL_MS = 20_000;\n\nexport interface InboxViewProps {\n /** Owner display name. Reserved for host wiring; not currently rendered. */\n ownerName?: string;\n /** Test/host injection seam. Defaults to the real `/api/lifeops/inbox` GET. */\n fetchers?: InboxFetchers;\n}\n\n// ---------------------------------------------------------------------------\n// Wire -> display DTO mapping.\n// ---------------------------------------------------------------------------\n\nconst KNOWN_CHANNELS: ReadonlySet<string> = new Set(INBOX_CHANNELS);\n\nfunction isKnownChannel(value: string): value is InboxChannel {\n return KNOWN_CHANNELS.has(value);\n}\n\nfunction mapMessage(message: InboxMessageWire): InboxItem | null {\n // The wire channel set is fixed; drop anything outside it rather than\n // rendering an unlabeled row. A dropped message means the server emitted a\n // channel this build doesn't know — surfaced as a smaller list, never a crash.\n if (!isKnownChannel(message.channel)) return null;\n return {\n id: message.id,\n channel: message.channel,\n sender: message.sender.displayName,\n subject: message.subject,\n preview: message.snippet,\n receivedAt: message.receivedAt,\n unread: message.unread,\n threadId: message.threadId ?? null,\n };\n}\n\n/** Channels with at least one message in the payload, in display order. */\nfunction connectedChannels(\n counts: Record<string, InboxChannelCountWire>,\n): InboxChannel[] {\n return INBOX_CHANNELS.filter((channel) => {\n const count = counts[channel];\n return count !== undefined && count.total > 0;\n });\n}\n\n/**\n * Proactive one-liner (DESIGN LAW 10): the agent noticing unread threads that\n * still need a reply. Returns null when nothing is unread so the line is absent\n * rather than reading \"0 threads\". Computed from the already-loaded items.\n */\nfunction unreadNudge(items: InboxItem[]): string | null {\n const unread = items.reduce((n, item) => (item.unread ? n + 1 : n), 0);\n if (unread === 0) return null;\n return `${unread} thread${unread === 1 ? \" still needs\" : \"s still need\"} a reply.`;\n}\n\n/** Single chat-handoff seam: search, reload, connect, and open all live in the\n * floating chat, so the view routes user intent there rather than computing. */\nfunction sendChatPrompt(prompt: string): void {\n // `client` is the shared ElizaClient; its published type does not surface\n // `sendChatMessage`, so read it through a narrow optional-method view (the\n // floating chat injects it at runtime) rather than widening the client type.\n (client as { sendChatMessage?: (text: string) => void }).sendChatMessage?.(\n prompt,\n );\n}\n\nfunction requestConnect(): void {\n sendChatPrompt(\"Connect a messaging channel so you can triage my inbox.\");\n}\n\nfunction requestOpen(item: InboxItem | undefined): void {\n if (!item) return;\n const title = item.subject ?? item.sender;\n sendChatPrompt(\n `Open the inbox thread from ${item.sender}${title ? ` — \"${title}\"` : \"\"}.`,\n );\n}\n\n// ---------------------------------------------------------------------------\n// Fetch-driven state machine.\n// ---------------------------------------------------------------------------\n\ninterface InboxData {\n items: InboxItem[];\n /** Channels that reported at least one message in the payload. */\n connected: InboxChannel[];\n}\n\ntype LoadState =\n | { kind: \"loading\" }\n | { kind: \"error\"; message: string }\n | { kind: \"ready\"; data: InboxData };\n\n/** Stable empty array so the pre-ready memo inputs keep a constant reference. */\nconst EMPTY_ITEMS: InboxItem[] = [];\n\nexport function InboxView(props: InboxViewProps = {}): ReactNode {\n const fetchers = props.fetchers ?? defaultFetchers;\n const [state, setState] = useState<LoadState>({ kind: \"loading\" });\n const [activeChannels, setActiveChannels] = useState<Set<InboxChannel>>(\n () => new Set<InboxChannel>(),\n );\n\n const fetchersRef = useRef(fetchers);\n fetchersRef.current = fetchers;\n\n // `background` skips the loading-state flash so the 20s poll refreshes the\n // already-rendered list in place; user-driven loads (mount, channel toggle,\n // retry) show the spinner.\n const load = useCallback((channels: InboxChannel[], background = false) => {\n let cancelled = false;\n if (!background) setState({ kind: \"loading\" });\n fetchersRef.current\n .fetchInbox(channels)\n .then((wire) => {\n if (cancelled) return;\n const items = wire.messages\n .map(mapMessage)\n .filter((item): item is InboxItem => item !== null);\n setState({\n kind: \"ready\",\n data: { items, connected: connectedChannels(wire.channelCounts) },\n });\n })\n .catch((error: unknown) => {\n if (cancelled) return;\n setState({\n kind: \"error\",\n message:\n error instanceof Error ? error.message : \"Could not load inbox.\",\n });\n });\n return () => {\n cancelled = true;\n };\n }, []);\n\n // Re-fetch with the server-side channel filter whenever the selection changes.\n // The active set is the single source of truth for both the query and the\n // client-side grouping, so the two can never disagree.\n const activeList = useMemo(\n () => INBOX_CHANNELS.filter((channel) => activeChannels.has(channel)),\n [activeChannels],\n );\n\n // Initial load + a quiet background poll keep the view fresh without a manual\n // refresh button (search and reload both live in the chat). The poll calls the\n // same load fn against the current channel selection; it's cleared on unmount\n // and re-armed whenever the selection changes.\n useEffect(() => {\n const cancelLoad = load(activeList);\n const timer = setInterval(() => load(activeList, true), INBOX_POLL_MS);\n return () => {\n cancelLoad();\n clearInterval(timer);\n };\n }, [load, activeList]);\n\n const items = state.kind === \"ready\" ? state.data.items : EMPTY_ITEMS;\n\n const onAction = useCallback(\n (action: string) => {\n if (action.startsWith(\"channel:\")) {\n const channel = action.slice(\"channel:\".length);\n if (!isKnownChannel(channel)) return;\n setActiveChannels((prev) => {\n const next = new Set(prev);\n if (next.has(channel)) next.delete(channel);\n else next.add(channel);\n return next;\n });\n return;\n }\n if (action.startsWith(\"open:\")) {\n const id = action.slice(\"open:\".length);\n requestOpen(items.find((item) => item.id === id));\n return;\n }\n switch (action) {\n case \"retry\":\n load(activeList);\n return;\n case \"connect\":\n requestConnect();\n return;\n }\n },\n [items, load, activeList],\n );\n\n const filters: InboxChannelFilter[] = useMemo(\n () =>\n INBOX_CHANNELS.map((channel) => ({\n channel,\n label: INBOX_CHANNEL_LABELS[channel],\n active: activeChannels.has(channel),\n })),\n [activeChannels],\n );\n\n const snapshot: InboxSnapshot = useMemo(() => {\n const status: InboxStatus =\n state.kind === \"loading\"\n ? \"loading\"\n : state.kind === \"error\"\n ? \"error\"\n : items.length === 0\n ? \"empty\"\n : \"ready\";\n return {\n status,\n items,\n filters,\n activeFilterCount: activeChannels.size,\n hasConnectedChannels:\n state.kind === \"ready\" && state.data.connected.length > 0,\n nudge: unreadNudge(items),\n error: state.kind === \"error\" ? state.message : null,\n };\n }, [state, items, filters, activeChannels]);\n\n return (\n <SpatialSurface>\n <InboxSpatialView snapshot={snapshot} onAction={onAction} />\n </SpatialSurface>\n );\n}\n\nexport default InboxView;\n"],"mappings":"AAwUM;AA/SN,SAAS,cAAc;AACvB,SAAS,sBAAsB;AAE/B,SAAS,aAAa,WAAW,SAAS,QAAQ,gBAAgB;AAClE;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AACP;AAAA,EAGE;AAAA,OAEK;AA8CP,eAAe,SAAS,UAA8C;AACpE,QAAM,SAAS,IAAI,gBAAgB;AACnC,MAAI,SAAS,SAAS,EAAG,QAAO,IAAI,YAAY,SAAS,KAAK,GAAG,CAAC;AAClE,QAAM,QAAQ,OAAO,SAAS;AAC9B,QAAM,OAAO,qBAAqB,QAAQ,IAAI,KAAK,KAAK,EAAE;AAC1D,QAAM,WAAW,MAAM,MAAM,GAAG,OAAO,WAAW,CAAC,GAAG,IAAI,EAAE;AAC5D,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,yBAAyB,SAAS,MAAM,GAAG;AAAA,EAC7D;AACA,SAAQ,MAAM,SAAS,KAAK;AAC9B;AAEA,MAAM,kBAAiC;AAAA,EACrC,YAAY;AACd;AAGA,MAAM,gBAAgB;AAatB,MAAM,iBAAsC,IAAI,IAAI,cAAc;AAElE,SAAS,eAAe,OAAsC;AAC5D,SAAO,eAAe,IAAI,KAAK;AACjC;AAEA,SAAS,WAAW,SAA6C;AAI/D,MAAI,CAAC,eAAe,QAAQ,OAAO,EAAG,QAAO;AAC7C,SAAO;AAAA,IACL,IAAI,QAAQ;AAAA,IACZ,SAAS,QAAQ;AAAA,IACjB,QAAQ,QAAQ,OAAO;AAAA,IACvB,SAAS,QAAQ;AAAA,IACjB,SAAS,QAAQ;AAAA,IACjB,YAAY,QAAQ;AAAA,IACpB,QAAQ,QAAQ;AAAA,IAChB,UAAU,QAAQ,YAAY;AAAA,EAChC;AACF;AAGA,SAAS,kBACP,QACgB;AAChB,SAAO,eAAe,OAAO,CAAC,YAAY;AACxC,UAAM,QAAQ,OAAO,OAAO;AAC5B,WAAO,UAAU,UAAa,MAAM,QAAQ;AAAA,EAC9C,CAAC;AACH;AAOA,SAAS,YAAY,OAAmC;AACtD,QAAM,SAAS,MAAM,OAAO,CAAC,GAAG,SAAU,KAAK,SAAS,IAAI,IAAI,GAAI,CAAC;AACrE,MAAI,WAAW,EAAG,QAAO;AACzB,SAAO,GAAG,MAAM,UAAU,WAAW,IAAI,iBAAiB,cAAc;AAC1E;AAIA,SAAS,eAAe,QAAsB;AAI5C,EAAC,OAAwD;AAAA,IACvD;AAAA,EACF;AACF;AAEA,SAAS,iBAAuB;AAC9B,iBAAe,yDAAyD;AAC1E;AAEA,SAAS,YAAY,MAAmC;AACtD,MAAI,CAAC,KAAM;AACX,QAAM,QAAQ,KAAK,WAAW,KAAK;AACnC;AAAA,IACE,8BAA8B,KAAK,MAAM,GAAG,QAAQ,YAAO,KAAK,MAAM,EAAE;AAAA,EAC1E;AACF;AAkBA,MAAM,cAA2B,CAAC;AAE3B,SAAS,UAAU,QAAwB,CAAC,GAAc;AAC/D,QAAM,WAAW,MAAM,YAAY;AACnC,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAoB,EAAE,MAAM,UAAU,CAAC;AACjE,QAAM,CAAC,gBAAgB,iBAAiB,IAAI;AAAA,IAC1C,MAAM,oBAAI,IAAkB;AAAA,EAC9B;AAEA,QAAM,cAAc,OAAO,QAAQ;AACnC,cAAY,UAAU;AAKtB,QAAM,OAAO,YAAY,CAAC,UAA0B,aAAa,UAAU;AACzE,QAAI,YAAY;AAChB,QAAI,CAAC,WAAY,UAAS,EAAE,MAAM,UAAU,CAAC;AAC7C,gBAAY,QACT,WAAW,QAAQ,EACnB,KAAK,CAAC,SAAS;AACd,UAAI,UAAW;AACf,YAAMA,SAAQ,KAAK,SAChB,IAAI,UAAU,EACd,OAAO,CAAC,SAA4B,SAAS,IAAI;AACpD,eAAS;AAAA,QACP,MAAM;AAAA,QACN,MAAM,EAAE,OAAAA,QAAO,WAAW,kBAAkB,KAAK,aAAa,EAAE;AAAA,MAClE,CAAC;AAAA,IACH,CAAC,EACA,MAAM,CAAC,UAAmB;AACzB,UAAI,UAAW;AACf,eAAS;AAAA,QACP,MAAM;AAAA,QACN,SACE,iBAAiB,QAAQ,MAAM,UAAU;AAAA,MAC7C,CAAC;AAAA,IACH,CAAC;AACH,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,CAAC;AAKL,QAAM,aAAa;AAAA,IACjB,MAAM,eAAe,OAAO,CAAC,YAAY,eAAe,IAAI,OAAO,CAAC;AAAA,IACpE,CAAC,cAAc;AAAA,EACjB;AAMA,YAAU,MAAM;AACd,UAAM,aAAa,KAAK,UAAU;AAClC,UAAM,QAAQ,YAAY,MAAM,KAAK,YAAY,IAAI,GAAG,aAAa;AACrE,WAAO,MAAM;AACX,iBAAW;AACX,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF,GAAG,CAAC,MAAM,UAAU,CAAC;AAErB,QAAM,QAAQ,MAAM,SAAS,UAAU,MAAM,KAAK,QAAQ;AAE1D,QAAM,WAAW;AAAA,IACf,CAAC,WAAmB;AAClB,UAAI,OAAO,WAAW,UAAU,GAAG;AACjC,cAAM,UAAU,OAAO,MAAM,WAAW,MAAM;AAC9C,YAAI,CAAC,eAAe,OAAO,EAAG;AAC9B,0BAAkB,CAAC,SAAS;AAC1B,gBAAM,OAAO,IAAI,IAAI,IAAI;AACzB,cAAI,KAAK,IAAI,OAAO,EAAG,MAAK,OAAO,OAAO;AAAA,cACrC,MAAK,IAAI,OAAO;AACrB,iBAAO;AAAA,QACT,CAAC;AACD;AAAA,MACF;AACA,UAAI,OAAO,WAAW,OAAO,GAAG;AAC9B,cAAM,KAAK,OAAO,MAAM,QAAQ,MAAM;AACtC,oBAAY,MAAM,KAAK,CAAC,SAAS,KAAK,OAAO,EAAE,CAAC;AAChD;AAAA,MACF;AACA,cAAQ,QAAQ;AAAA,QACd,KAAK;AACH,eAAK,UAAU;AACf;AAAA,QACF,KAAK;AACH,yBAAe;AACf;AAAA,MACJ;AAAA,IACF;AAAA,IACA,CAAC,OAAO,MAAM,UAAU;AAAA,EAC1B;AAEA,QAAM,UAAgC;AAAA,IACpC,MACE,eAAe,IAAI,CAAC,aAAa;AAAA,MAC/B;AAAA,MACA,OAAO,qBAAqB,OAAO;AAAA,MACnC,QAAQ,eAAe,IAAI,OAAO;AAAA,IACpC,EAAE;AAAA,IACJ,CAAC,cAAc;AAAA,EACjB;AAEA,QAAM,WAA0B,QAAQ,MAAM;AAC5C,UAAM,SACJ,MAAM,SAAS,YACX,YACA,MAAM,SAAS,UACb,UACA,MAAM,WAAW,IACf,UACA;AACV,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA,mBAAmB,eAAe;AAAA,MAClC,sBACE,MAAM,SAAS,WAAW,MAAM,KAAK,UAAU,SAAS;AAAA,MAC1D,OAAO,YAAY,KAAK;AAAA,MACxB,OAAO,MAAM,SAAS,UAAU,MAAM,UAAU;AAAA,IAClD;AAAA,EACF,GAAG,CAAC,OAAO,OAAO,SAAS,cAAc,CAAC;AAE1C,SACE,oBAAC,kBACC,8BAAC,oBAAiB,UAAoB,UAAoB,GAC5D;AAEJ;AAEA,IAAO,oBAAQ;","names":["items"]}
@@ -0,0 +1,2 @@
1
+ export { InboxView } from "./InboxView.js";
2
+ //# sourceMappingURL=inbox-view-bundle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbox-view-bundle.d.ts","sourceRoot":"","sources":["../../../src/components/inbox/inbox-view-bundle.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { InboxView } from "./InboxView.js";
2
+ export {
3
+ InboxView
4
+ };
5
+ //# sourceMappingURL=inbox-view-bundle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/components/inbox/inbox-view-bundle.ts"],"sourcesContent":["// Vite view-bundle entry. Re-exports the InboxView component so the built\n// bundle (dist/views/bundle.js) exposes the named export the view loader reads.\n// Kept separate from InboxView.tsx so that file exports only React components\n// and stays Fast-Refresh-compatible in dev.\nexport { InboxView } from \"./InboxView.js\";\n"],"mappings":"AAIA,SAAS,iBAAiB;","names":[]}
@@ -0,0 +1,3 @@
1
+ export * from "./schema.js";
2
+ export * from "./sql.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/db/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from "./schema.js";
2
+ export * from "./sql.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/db/index.ts"],"sourcesContent":["export * from \"./schema.js\";\nexport * from \"./sql.js\";\n"],"mappings":"AAAA,cAAc;AACd,cAAc;","names":[]}