@alia.onl/sdk 15.7.0 → 19.0.0

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 (150) hide show
  1. package/CHANGELOG.md +255 -12
  2. package/README.md +241 -155
  3. package/package.json +17 -5
  4. package/src/animations.ts +0 -1
  5. package/src/components/AliaAskProvider.tsx +127 -0
  6. package/src/components/AliaChatContent.tsx +604 -392
  7. package/src/components/AliaChatHeader.tsx +14 -38
  8. package/src/components/AliaChatScreen.tsx +56 -31
  9. package/src/components/AliaChatSheet.tsx +110 -36
  10. package/src/components/AliaComposer.tsx +1 -1
  11. package/src/components/AliaParticipants.tsx +202 -0
  12. package/src/components/AliaParticipantsHeader.tsx +60 -0
  13. package/src/components/AliaSharedConversation.tsx +86 -0
  14. package/src/components/AliaWelcomeMessage.tsx +18 -5
  15. package/src/components/Image.tsx +12 -0
  16. package/src/components/Markdown.tsx +498 -14
  17. package/src/components/PlanPreviewCard.tsx +7 -49
  18. package/src/components/ReplyMarkdown.tsx +359 -0
  19. package/src/components/RichBlocks.tsx +195 -0
  20. package/src/components/canvas/CanvasComponent.tsx +59 -0
  21. package/src/components/canvas/ChartRenderer.tsx +73 -0
  22. package/src/components/canvas/CodeRenderer.tsx +24 -0
  23. package/src/components/canvas/FormRenderer.tsx +101 -0
  24. package/src/components/canvas/MarkdownRenderer.tsx +14 -0
  25. package/src/components/canvas/TableRenderer.tsx +47 -0
  26. package/src/components/thought/FilesAndSources.tsx +120 -0
  27. package/src/components/thought/ThoughtPanel.tsx +417 -0
  28. package/src/components/thought/ToolStep.tsx +141 -0
  29. package/src/components/thread/AgentResultCard.tsx +125 -0
  30. package/src/components/thread/AgentTaskCard.tsx +127 -0
  31. package/src/components/thread/AliaThread.tsx +401 -0
  32. package/src/components/thread/MessageBlockBoundary.tsx +78 -0
  33. package/src/components/thread/NewConversationOffer.tsx +54 -0
  34. package/src/components/thread/ThreadRow.tsx +458 -0
  35. package/src/components/thread/ThreadSearch.tsx +129 -0
  36. package/src/components/thread/ToolResultCard.tsx +254 -0
  37. package/src/components/thread/TurnMenu.tsx +54 -0
  38. package/src/components/thread/TurnStatusLine.tsx +34 -0
  39. package/src/components/voice/AliaVoiceCall.tsx +21 -0
  40. package/src/components/voice/VoiceControls.tsx +2 -28
  41. package/src/components/voice/VoiceSession.tsx +39 -11
  42. package/src/csp.ts +50 -0
  43. package/src/hooks/useAliaActivity.ts +3 -3
  44. package/src/hooks/useAtBottom.ts +51 -0
  45. package/src/hooks/useAttachMenu.ts +94 -0
  46. package/src/hooks/useChatComposer.ts +299 -0
  47. package/src/hooks/useChatVoice.ts +129 -0
  48. package/src/hooks/useDictation.ts +2 -3
  49. package/src/hooks/useDocumentPicker.ts +3 -29
  50. package/src/hooks/useFailedTurnToast.ts +43 -0
  51. package/src/hooks/useMessageVote.ts +50 -0
  52. package/src/hooks/useOpenThought.ts +79 -0
  53. package/src/hooks/usePendingQuestions.ts +2 -2
  54. package/src/hooks/useReadAloud.ts +92 -0
  55. package/src/hooks/useShareHandler.ts +24 -0
  56. package/src/hooks/useSoundEffects.ts +1 -1
  57. package/src/hooks/useTTS.ts +2 -3
  58. package/src/hooks/useThoughtScope.ts +59 -0
  59. package/src/hooks/useThreadRows.ts +145 -0
  60. package/src/hooks/useThreadView.ts +68 -0
  61. package/src/hooks/useTimelineWindow.ts +256 -0
  62. package/src/hooks/useVoiceLauncher.ts +180 -0
  63. package/src/hooks/useVoiceRoom.ts +2 -6
  64. package/src/index.ts +43 -29
  65. package/src/lib/activity-store.ts +1 -1
  66. package/src/lib/add-menu.ts +7 -0
  67. package/src/lib/call-thread.ts +33 -0
  68. package/src/lib/character-runtime.ts +6 -4
  69. package/src/lib/citations.ts +79 -2
  70. package/src/lib/clipboard.ts +23 -0
  71. package/src/lib/content-utils.ts +10 -0
  72. package/src/lib/copy-en.ts +284 -0
  73. package/src/lib/copy.ts +32 -0
  74. package/src/lib/document-picker.ts +31 -0
  75. package/src/lib/flights.ts +22 -13
  76. package/src/lib/focus-return.ts +66 -0
  77. package/src/lib/keyboard.native.tsx +19 -0
  78. package/src/lib/keyboard.tsx +9 -0
  79. package/src/lib/labels.tsx +69 -17
  80. package/src/lib/origins.ts +15 -0
  81. package/src/lib/participants.ts +86 -0
  82. package/src/lib/power-level-options.ts +66 -0
  83. package/src/lib/questions.ts +2 -2
  84. package/src/lib/task-labels.ts +4 -4
  85. package/src/lib/tool-call-status.ts +1 -1
  86. package/src/lib/tool-icons.ts +36 -0
  87. package/src/lib/tool-renderers.tsx +48 -0
  88. package/src/lib/turn-state.ts +2 -2
  89. package/src/lib/voice-audio-engine.ts +14 -158
  90. package/src/lib/voice-call-request.ts +49 -0
  91. package/src/lib/voice-call.ts +76 -34
  92. package/src/lib/voice-controls-labels.ts +31 -0
  93. package/src/lib/voice-protocol.ts +32 -14
  94. package/src/lib/voice-worklets.ts +19 -0
  95. package/src/lib/work-log.ts +6 -6
  96. package/src/runtime/agents.ts +151 -0
  97. package/src/runtime/api.ts +123 -0
  98. package/src/runtime/attachment-utils.ts +96 -0
  99. package/src/runtime/composer-draft-store.ts +270 -0
  100. package/src/runtime/config.tsx +186 -0
  101. package/src/runtime/conversation-store.ts +49 -0
  102. package/src/runtime/conversations.ts +557 -0
  103. package/src/runtime/error-utils.ts +104 -0
  104. package/src/runtime/host-tools.ts +140 -0
  105. package/src/runtime/index.ts +113 -0
  106. package/src/runtime/message-history.ts +124 -0
  107. package/src/runtime/model.ts +39 -0
  108. package/src/runtime/oxy-files.ts +37 -0
  109. package/src/runtime/power-levels.ts +66 -0
  110. package/src/runtime/query-keys.ts +34 -0
  111. package/src/runtime/random-uuid.ts +25 -0
  112. package/src/runtime/routes.ts +51 -0
  113. package/src/runtime/sharing.ts +165 -0
  114. package/src/runtime/sse-frame-reader.ts +88 -0
  115. package/src/runtime/stream-events.ts +146 -0
  116. package/src/runtime/stream-outcome.ts +21 -0
  117. package/src/runtime/thought-store.ts +103 -0
  118. package/src/runtime/thread/agent-activity.ts +261 -0
  119. package/src/runtime/thread/automation-labels.ts +79 -0
  120. package/src/runtime/thread/conversation-export.ts +135 -0
  121. package/src/runtime/thread/message-days.ts +120 -0
  122. package/src/runtime/thread/message-processor.ts +93 -0
  123. package/src/runtime/thread/task-utils.ts +11 -0
  124. package/src/runtime/thread/thought-utils.ts +752 -0
  125. package/src/runtime/thread/thread-history.ts +139 -0
  126. package/src/runtime/thread/timeline.ts +202 -0
  127. package/src/runtime/thread/title-tags.ts +8 -0
  128. package/src/runtime/thread/tool-cards.ts +383 -0
  129. package/src/runtime/turn-failure.ts +108 -0
  130. package/src/runtime/types.ts +148 -0
  131. package/src/runtime/usage-limit-error.ts +44 -0
  132. package/src/runtime/use-agent-activity.ts +80 -0
  133. package/src/runtime/use-conversation-stream.ts +1427 -0
  134. package/src/runtime/use-conversation-thread.ts +556 -0
  135. package/src/runtime/use-start-conversation.ts +107 -0
  136. package/src/runtime/use-thread-history.ts +64 -0
  137. package/src/runtime/use-thread-search.ts +95 -0
  138. package/src/runtime/use-turn-edit.ts +99 -0
  139. package/src/tools.ts +2 -2
  140. package/src/types.ts +18 -47
  141. package/worklets/voice-processors.js +152 -0
  142. package/src/components/AliaChatMessageList.tsx +0 -532
  143. package/src/components/ThinkingBlock.tsx +0 -43
  144. package/src/hooks/useAliaChat.ts +0 -573
  145. package/src/lib/api-url.ts +0 -2
  146. package/src/lib/catalogue.ts +0 -236
  147. package/src/lib/chat-stream.ts +0 -655
  148. package/src/lib/chat-transport.ts +0 -98
  149. package/src/lib/delegation.ts +0 -34
  150. package/src/lib/work-log-copy.ts +0 -61
@@ -0,0 +1,139 @@
1
+ import type { Message } from '../types';
2
+
3
+ /**
4
+ * The history of a thread: everything said before the stretch on screen.
5
+ *
6
+ * ## A thread is many conversations, and the screen only holds one
7
+ *
8
+ * `/@pepe` shows one continuous history, but underneath it is a run of ordinary
9
+ * conversations sharing an `agentId`, and the chat screen is streaming into the
10
+ * most recent one. `GET /agents/thread/:username/messages` is what crosses
11
+ * those seams; this module turns its pages into the list that goes above the
12
+ * live messages, and says where the joins are.
13
+ *
14
+ * Everything here is pure. The fetching lives in `use-thread-history.ts` and
15
+ * `use-thread-search.ts`; the scrolling and the anchoring are Bloom's `AiChatThread`'s.
16
+ */
17
+
18
+ /**
19
+ * A message as the thread endpoint sends it.
20
+ *
21
+ * Two fields more than an ordinary message, and neither is decorative:
22
+ * `conversationId` is the only thing a client can draw a seam from — there is
23
+ * no table of breaks — and `cursor` addresses this exact message, which is what
24
+ * makes both paging and jumping to a search hit possible.
25
+ *
26
+ * `id` is OPTIONAL on the wire, because it is the CLIENT's id and a message
27
+ * written by the server never had one.
28
+ */
29
+ export interface WireThreadMessage extends Omit<Message, 'id'> {
30
+ id?: string;
31
+ conversationId: string;
32
+ cursor: string;
33
+ }
34
+
35
+ /** What is behind the conversation on screen: the rest of an agent's thread. */
36
+ export interface ThreadHistory {
37
+ /** Everything said before the conversation on screen, oldest first. */
38
+ messages: ThreadMessage[];
39
+ /** A page is on its way; the reader is at the top waiting for it. */
40
+ loading: boolean;
41
+ /** Ask for the page above. Absent where nothing older remains, which also turns the anchor off. */
42
+ loadMore?: () => void;
43
+ }
44
+
45
+ /**
46
+ * One hit of a search in a thread. `messageId` is the client's id, which a message the server
47
+ * wrote has none of.
48
+ */
49
+ export interface ThreadSearchHit {
50
+ messageId: string | null;
51
+ conversationId: string;
52
+ role: string;
53
+ snippet: string;
54
+ createdAt: string;
55
+ /** Where this message is, in the only form that can be asked for. */
56
+ cursor: string;
57
+ }
58
+
59
+ /** One page of history, oldest-first, with the cursor for the page above it. */
60
+ export interface ThreadPage {
61
+ messages: readonly WireThreadMessage[];
62
+ /** `null` when nothing older remains. Never inferred from a page's length. */
63
+ nextCursor: string | null;
64
+ }
65
+
66
+ /** A history message, once it is sure to have an id to be keyed and voted by. */
67
+ export interface ThreadMessage extends Message {
68
+ conversationId: string;
69
+ cursor: string;
70
+ }
71
+
72
+ /**
73
+ * The pages, flattened into the messages that go ABOVE the live conversation.
74
+ *
75
+ * Two orderings meet here and they run opposite ways: pages arrive newest-first
76
+ * (each one is the window before the last), while the messages inside a page
77
+ * are oldest-first. So the pages are walked backwards and their contents
78
+ * forwards, which is what puts the whole thing in reading order.
79
+ *
80
+ * **The active stretch is dropped.** Its messages are already on screen, held
81
+ * by the streaming hook, and they are the only ones that can still change —
82
+ * keeping the endpoint's copy too would show every one of them twice and freeze
83
+ * the duplicate at the state it was fetched in. Dropping them here is also what
84
+ * makes starting a new stretch work for free: the messages of the conversation
85
+ * that was active a moment ago stop matching, so they reappear as history
86
+ * without a second request.
87
+ *
88
+ * The cursor stands in for a missing id. A server-written message has no client
89
+ * id, `id` is what the list keys on and what the vote URL carries, and a cursor
90
+ * is unique to one message by construction.
91
+ */
92
+ export function threadHistory(
93
+ pages: readonly ThreadPage[],
94
+ activeConversationId: string,
95
+ ): ThreadMessage[] {
96
+ const history: ThreadMessage[] = [];
97
+ for (let page = pages.length - 1; page >= 0; page -= 1) {
98
+ for (const message of pages[page].messages) {
99
+ if (message.conversationId === activeConversationId) continue;
100
+ history.push({ ...message, id: message.id ?? message.cursor });
101
+ }
102
+ }
103
+ return history;
104
+ }
105
+
106
+ /**
107
+ * Which messages BEGIN a new conversation, keyed by id.
108
+ *
109
+ * A seam is deduced from the data — the conversation a message belongs to
110
+ * changed — and never from elapsed time. "More than N hours passed" is the
111
+ * tempting version and it lies the day somebody returns a week later to finish
112
+ * the same thought: the model is still being given that conversation, so the
113
+ * screen would draw a break where the context has none.
114
+ *
115
+ * The topmost loaded message never carries one, deliberately: whether a break
116
+ * sits above it is a fact about the page that has not been loaded yet. It
117
+ * appears when that page arrives, which is the only moment it is known.
118
+ *
119
+ * The live messages are one stretch, so at most one seam concerns them — above
120
+ * the first — and it is drawn only when there is history above to be separated
121
+ * from.
122
+ */
123
+ export function threadSeamIds(
124
+ history: readonly ThreadMessage[],
125
+ live: readonly { id: string }[],
126
+ activeConversationId: string,
127
+ ): Set<string> {
128
+ const seams = new Set<string>();
129
+ let previous: string | null = null;
130
+ for (const message of history) {
131
+ if (previous !== null && message.conversationId !== previous) seams.add(message.id);
132
+ previous = message.conversationId;
133
+ }
134
+ const firstLive = live[0];
135
+ if (firstLive !== undefined && previous !== null && previous !== activeConversationId) {
136
+ seams.add(firstLive.id);
137
+ }
138
+ return seams;
139
+ }
@@ -0,0 +1,202 @@
1
+ import type { Message } from '../types';
2
+
3
+ /**
4
+ * The chat timeline as data: which rows are mounted, which just arrived, and
5
+ * which facts about the list a streamed token can and cannot change.
6
+ *
7
+ * Everything here is pure. `hooks/useTimelineWindow.ts` holds the state
8
+ * and talks to the thread; `components/thread/AliaThread.tsx` draws the rows.
9
+ *
10
+ * ## Why a paged window and not virtualization
11
+ *
12
+ * Bloom's `AiChatThread` is a plain `ScrollView` on native and the document on
13
+ * web, with no virtualization and no `scrollToIndex` (#608, gap 3 in
14
+ * `docs/bloom-adoption.mdx`). A spacer-based virtualizer on top of it would
15
+ * swap estimated heights for measured ones ABOVE the reader, and the thread
16
+ * only compensates growth it asked for — so every estimate that was wrong
17
+ * would move the text being read. What the thread does anchor exactly is a
18
+ * page landing above after `onStartReached`, and that is the one move this
19
+ * window makes: it mounts the newest rows, and reveals older ones a page at a
20
+ * time when the reader comes near the top, through the same anchored growth
21
+ * history paging already uses. Rows are only ever dropped from the top while
22
+ * the reader is at the bottom, where nothing above them is on screen.
23
+ */
24
+
25
+ /** How many rows a thread opens with, counted from the newest. */
26
+ export const INITIAL_ROWS = 40;
27
+ /** How many older rows each approach to the top reveals. */
28
+ export const PAGE_ROWS = 30;
29
+ /** Rows kept above a cursor-jump target, so it does not land on the window's edge. */
30
+ export const FOCUS_MARGIN = 5;
31
+ /**
32
+ * How far past its opening size the window may grow before it is folded back.
33
+ * Folding is only done at the bottom with no turn in flight, so a reader who
34
+ * went up a long way does not keep every row they passed mounted forever.
35
+ */
36
+ export const COLLAPSE_AFTER = INITIAL_ROWS + 2 * PAGE_ROWS;
37
+
38
+ /**
39
+ * Where the mounted range starts.
40
+ *
41
+ * - `tail`: the newest `INITIAL_ROWS`. Only held until the rows are known;
42
+ * the hook pins it to a row as soon as there are rows, so rows appended by
43
+ * a turn grow the window instead of sliding it.
44
+ * - `top`: from the first row loaded. Set when the reader has reached the top
45
+ * and asked for history, so the page that lands is shown.
46
+ * - `row`: from this row, by id — ids survive rows being added on either side.
47
+ */
48
+ export type TimelineAnchor = { kind: 'tail' } | { kind: 'top' } | { kind: 'row'; id: string };
49
+
50
+ export const TAIL: TimelineAnchor = Object.freeze({ kind: 'tail' });
51
+ export const TOP: TimelineAnchor = Object.freeze({ kind: 'top' });
52
+
53
+ /** The index of the first mounted row. */
54
+ export function resolveStart(
55
+ anchor: TimelineAnchor,
56
+ ids: readonly string[],
57
+ focusIndex = -1,
58
+ ): number {
59
+ let start: number;
60
+ if (anchor.kind === 'top') start = 0;
61
+ else if (anchor.kind === 'row') {
62
+ const index = ids.indexOf(anchor.id);
63
+ // A row that is gone (a refetched history, a replaced conversation) falls
64
+ // back to the newest page rather than to everything.
65
+ start = index === -1 ? tailStart(ids.length) : index;
66
+ } else start = tailStart(ids.length);
67
+ // A jump's target is always mounted, with a few rows of context above it.
68
+ if (focusIndex !== -1) start = Math.min(start, Math.max(0, focusIndex - FOCUS_MARGIN));
69
+ return start;
70
+ }
71
+
72
+ function tailStart(count: number): number {
73
+ return Math.max(0, count - INITIAL_ROWS);
74
+ }
75
+
76
+ /** The anchor that keeps exactly the rows mounted now, from the row at `start`. */
77
+ export function pinAnchor(ids: readonly string[], start: number): TimelineAnchor {
78
+ if (start <= 0 || ids.length === 0) return TOP;
79
+ return { kind: 'row', id: ids[start] };
80
+ }
81
+
82
+ /**
83
+ * The reader came near the top: one more page of the rows already loaded, or,
84
+ * with none left above, `null` — the thread's own history is what comes next.
85
+ */
86
+ export function revealAbove(ids: readonly string[], start: number): TimelineAnchor | null {
87
+ if (start <= 0) return null;
88
+ return pinAnchor(ids, Math.max(0, start - PAGE_ROWS));
89
+ }
90
+
91
+ /** Fold a window that grew back to its opening size, or `null` if it has not grown enough. */
92
+ export function collapseToTail(ids: readonly string[], start: number): TimelineAnchor | null {
93
+ if (ids.length - start <= COLLAPSE_AFTER) return null;
94
+ return pinAnchor(ids, tailStart(ids.length));
95
+ }
96
+
97
+ /**
98
+ * The facts about a message the timeline's derived data is built from — the
99
+ * day separators, the seams, the turn timings, which answer is the last of
100
+ * Alia's — and nothing a token changes.
101
+ */
102
+ type Shape = Pick<Message, 'id' | 'role' | 'createdAt' | 'agentInfo'>;
103
+
104
+ function sameShape(a: Shape, b: Shape): boolean {
105
+ return (
106
+ a === b ||
107
+ (a.id === b.id &&
108
+ a.role === b.role &&
109
+ a.createdAt === b.createdAt &&
110
+ a.agentInfo?.id === b.agentInfo?.id)
111
+ );
112
+ }
113
+
114
+ /**
115
+ * Whether two versions of the list differ only in what their messages SAY.
116
+ *
117
+ * A streamed token replaces the last message object and the array around it,
118
+ * so identity alone says "changed" twenty times a second. This compares the
119
+ * fields the derived data reads; the per-element check is a pointer comparison
120
+ * for every message but the one being written, which is why it costs next to
121
+ * nothing where recomputing the derivations cost a pass over the whole thread.
122
+ */
123
+ export function sameTimelineShape(previous: readonly Shape[], next: readonly Shape[]): boolean {
124
+ if (previous === next) return true;
125
+ if (previous.length !== next.length) return false;
126
+ for (let i = 0; i < next.length; i += 1) {
127
+ if (!sameShape(previous[i], next[i])) return false;
128
+ }
129
+ return true;
130
+ }
131
+
132
+ /**
133
+ * The ids that ARRIVED in this render — the rows that may animate in.
134
+ *
135
+ * Not "every id the last render did not have". Three things change the list
136
+ * without anything arriving, and each would otherwise replay every message as
137
+ * if it had just been said:
138
+ *
139
+ * - the first render of a screen: everything on it is already there;
140
+ * - switching conversation in place: the list is REPLACED, so nothing it
141
+ * shares with the last one is the tell;
142
+ * - a conversation's messages landing after its load: the list fills from
143
+ * empty in one batch, where a send adds a question and the answer's
144
+ * placeholder — never more than two.
145
+ */
146
+ export function arrivedIds(
147
+ previous: readonly string[] | null,
148
+ previousLoading: boolean,
149
+ next: readonly { id: string }[],
150
+ ): ReadonlySet<string> {
151
+ if (previous === null || previousLoading) return EMPTY_IDS;
152
+ const known = new Set(previous);
153
+ const arrived = next.filter((m) => !known.has(m.id));
154
+ if (arrived.length === 0) return EMPTY_IDS;
155
+ const replaced = previous.length > 0 && arrived.length === next.length;
156
+ const loaded = previous.length === 0 && arrived.length > 2;
157
+ if (replaced || loaded) return EMPTY_IDS;
158
+ return new Set(arrived.map((m) => m.id));
159
+ }
160
+
161
+ const EMPTY_IDS: ReadonlySet<string> = new Set();
162
+
163
+ /**
164
+ * Where the reader was in a conversation, kept for the session so returning to
165
+ * it lands there instead of at whatever the scroller last held.
166
+ *
167
+ * `probeId` is a row that was mounted when this was saved. The position is only
168
+ * restored once that row is on screen again, because until then the list may
169
+ * still be the previous conversation's — the streaming hook swaps its messages
170
+ * in an effect, a render after the conversation id changes.
171
+ */
172
+ export interface TimelinePosition {
173
+ anchor: TimelineAnchor;
174
+ /** px from the thread's top, in the frame `scrollToOffset` takes. */
175
+ offset: number;
176
+ atBottom: boolean;
177
+ probeId: string;
178
+ }
179
+
180
+ /**
181
+ * The session's saved positions, by conversation. Never persisted: a reload
182
+ * opens at the newest turn. Not partitioned by account either, because a
183
+ * conversation id belongs to one account and an offset says nothing about what
184
+ * the conversation contains.
185
+ */
186
+ export class TimelinePositions {
187
+ private readonly positions = new Map<string, TimelinePosition>();
188
+
189
+ save(key: string, position: TimelinePosition): void {
190
+ this.positions.set(key, position);
191
+ }
192
+
193
+ read(key: string): TimelinePosition | undefined {
194
+ return this.positions.get(key);
195
+ }
196
+
197
+ clear(): void {
198
+ this.positions.clear();
199
+ }
200
+ }
201
+
202
+ export const timelinePositions = new TimelinePositions();
@@ -0,0 +1,8 @@
1
+ // Known translations of "TITLE" that LLMs may produce
2
+ const TAG = String.raw`ALIA_TITLE|TITLE|TÍTULO|TITRE|TITOLO|TITEL|ЗАГОЛОВОК`;
3
+
4
+ /** Matches complete [TITLE]...[/TITLE] and <TITLE>...</TITLE> tags (including translated variants) */
5
+ export const TITLE_STRIP_RE = new RegExp(
6
+ String.raw`\[(${TAG})\].*?\[\/\1\]|<(${TAG})>.*?<\/\2>`,
7
+ 'gi',
8
+ );