@djangocfg/ui-tools 2.1.375 → 2.1.377

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/ui-tools",
3
- "version": "2.1.375",
3
+ "version": "2.1.377",
4
4
  "description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
5
5
  "keywords": [
6
6
  "ui-tools",
@@ -156,8 +156,8 @@
156
156
  "check": "tsc --noEmit"
157
157
  },
158
158
  "peerDependencies": {
159
- "@djangocfg/i18n": "^2.1.375",
160
- "@djangocfg/ui-core": "^2.1.375",
159
+ "@djangocfg/i18n": "^2.1.377",
160
+ "@djangocfg/ui-core": "^2.1.377",
161
161
  "consola": "^3.4.2",
162
162
  "lodash-es": "^4.18.1",
163
163
  "lucide-react": "^0.545.0",
@@ -211,10 +211,10 @@
211
211
  "material-file-icons": "^2.4.0"
212
212
  },
213
213
  "devDependencies": {
214
- "@djangocfg/i18n": "^2.1.375",
214
+ "@djangocfg/i18n": "^2.1.377",
215
215
  "@djangocfg/playground": "workspace:*",
216
- "@djangocfg/typescript-config": "^2.1.375",
217
- "@djangocfg/ui-core": "^2.1.375",
216
+ "@djangocfg/typescript-config": "^2.1.377",
217
+ "@djangocfg/ui-core": "^2.1.377",
218
218
  "@types/lodash-es": "^4.17.12",
219
219
  "@types/mapbox__mapbox-gl-draw": "^1.4.8",
220
220
  "@types/node": "^24.7.2",
@@ -342,6 +342,48 @@ Options:
342
342
  The hook only fires on the `true → false` edge — flipping `enabled`
343
343
  mid-stream won't steal focus.
344
344
 
345
+ ## Draft sanitation (pre-submit)
346
+
347
+ `useChatComposer.submit()` cleans the draft before handing it to your
348
+ `onSubmit` — mirroring what ChatGPT / Claude / Telegram ship. The
349
+ helper is also exported standalone for custom composers:
350
+
351
+ ```ts
352
+ import { sanitizeDraft, isSubmittableDraft } from '@djangocfg/ui-tools'
353
+
354
+ sanitizeDraft(' \n\nhello world​\n ') // → 'hello world'
355
+ isSubmittableDraft(' \n ') // → false
356
+ ```
357
+
358
+ ### Rules (intentionally minimal)
359
+
360
+ | Rule | Action | Why |
361
+ |---|---|---|
362
+ | Trim outer whitespace | ✅ | Stray newlines/spaces at the edges are never intentional. |
363
+ | Normalise `\r\n` / `\r` → `\n` | ✅ | Deterministic shape for LLM tokeniser + markdown render. |
364
+ | Strip ZWSP / ZWNJ / ZWJ / BOM | ✅ | Pasted from rich web pages, invisible, break tokenisation. |
365
+ | **Collapse internal whitespace runs** | ❌ | Would mangle code indentation. |
366
+ | **Cap consecutive blank lines** | ❌ | Could be intentional separator (markdown / structured prompt). |
367
+ | **Strip bidi overrides** (LRM/RLM/U+202A..E) | ❌ | Legitimately used in Arabic/Hebrew RTL content. |
368
+ | Touch tabs vs spaces, emoji, mentions, URLs | ❌ | Passthrough. |
369
+
370
+ Idempotent: `sanitizeDraft(sanitizeDraft(x)) === sanitizeDraft(x)`.
371
+
372
+ ### Escape hatch — `preserveExactValue`
373
+
374
+ Niche flows (clipboard inspector, raw-prompt debug tool) want
375
+ byte-perfect passthrough. Opt out per-composer:
376
+
377
+ ```ts
378
+ const composer = useChatComposer({
379
+ onSubmit,
380
+ preserveExactValue: true, // skip sanitation; raw textarea value goes through
381
+ })
382
+ ```
383
+
384
+ `canSubmit` still gates on `value.trim().length > 0` in this mode —
385
+ an empty message is rarely intentional even when sanitation is off.
386
+
345
387
  ## Attachment renderers (registry)
346
388
 
347
389
  `<Attachments>` and `<ChatRoot>` accept a per-type renderer map. Default tile is used when no renderer matches. Plug in heavy viewers (`LazyAudioPlayer`, `LazyImageViewer`, `LazyMap`) host-side without forcing `ui-tools/Chat` to depend on them.
@@ -532,6 +574,9 @@ type ToolPayloadMatcher, type ToolPayloadFallback
532
574
  // Lightbox helpers
533
575
  collectImageAttachments
534
576
 
577
+ // Draft sanitation (pre-submit cleanup; see "Draft sanitation" above)
578
+ sanitizeDraft, isSubmittableDraft
579
+
535
580
  // Context
536
581
  ChatProvider, useChatContext, useChatContextOptional,
537
582
  type ComposerHandle
@@ -563,6 +608,12 @@ LazyChat
563
608
  | `↓` | Recall next |
564
609
  | `Esc` | (host-bound) cancel stream |
565
610
 
611
+ > **Custom composers built on `MarkdownEditor`:** pass `onSubmit` to
612
+ > the editor for the same behaviour. A React `onKeyDown` wrapper does
613
+ > NOT reliably intercept Enter before Tiptap commits the HardBreak
614
+ > — see `MarkdownEditor/README.md#submit-on-enter` for the full
615
+ > incident write-up and the keymap-extension fix.
616
+
566
617
  ## Performance
567
618
 
568
619
  - **Token coalescing.** `createTokenBuffer` aggregates stream chunks within ~16ms before dispatching → ≤1 render per frame.
@@ -13,6 +13,7 @@ import {
13
13
 
14
14
  import type { ChatAttachment } from '../types';
15
15
  import { LIMITS } from '../config';
16
+ import { sanitizeDraft } from '../utils/sanitizeDraft';
16
17
 
17
18
  export interface UseChatComposerOptions {
18
19
  onSubmit: (content: string, attachments: ChatAttachment[]) => void | Promise<void>;
@@ -34,6 +35,14 @@ export interface UseChatComposerOptions {
34
35
  * exactly as before. Plan64.
35
36
  */
36
37
  persistKey?: string;
38
+ /**
39
+ * Skip pre-submit draft sanitation (trim + line-ending normalise +
40
+ * zero-width strip). Default `false` — sanitation matches what
41
+ * ChatGPT / Claude / Telegram ship and is what consumers want 99% of
42
+ * the time. Set to `true` for niche flows that need byte-perfect
43
+ * passthrough (clipboard inspector, raw-prompt debug tool).
44
+ */
45
+ preserveExactValue?: boolean;
37
46
  }
38
47
 
39
48
  export interface UseChatComposerReturn {
@@ -73,6 +82,7 @@ export function useChatComposer(options: UseChatComposerOptions): UseChatCompose
73
82
  history = { enabled: true, size: LIMITS.composerHistorySize },
74
83
  onPasteFiles,
75
84
  persistKey,
85
+ preserveExactValue = false,
76
86
  } = options;
77
87
 
78
88
  // Hydrate draft from sessionStorage on mount when a key is provided.
@@ -135,26 +145,39 @@ export function useChatComposer(options: UseChatComposerOptions): UseChatCompose
135
145
  }, []);
136
146
 
137
147
  const submit = useCallback(async () => {
138
- const trimmed = value.trim();
139
- if ((!trimmed && attachments.length === 0) || isSubmitting || disabled) return;
148
+ // Sanitise BEFORE the empty-guard so a draft of only whitespace
149
+ // (` \n\n `) is treated as empty saves a round trip for
150
+ // "send" attempts that would produce nothing.
151
+ //
152
+ // sanitizeDraft is intentionally conservative: trim outer
153
+ // whitespace, normalise line endings, strip zero-width chars.
154
+ // It does NOT collapse internal whitespace or cap blank lines
155
+ // (would break code indentation / intentional separators). See
156
+ // utils/sanitizeDraft.ts for the full ruleset + rationale.
157
+ //
158
+ // The cleaned text is what reaches `onSubmit` (matches ChatGPT /
159
+ // Claude / Telegram behaviour — the bubble shows what the user
160
+ // last saw, sans accidental trailing whitespace). Niche callers
161
+ // can opt out via `preserveExactValue`.
162
+ const cleaned = preserveExactValue ? value : sanitizeDraft(value);
163
+ if ((!cleaned && attachments.length === 0) || isSubmitting || disabled) return;
140
164
  setIsSubmitting(true);
141
165
  try {
142
- if (history.enabled !== false && trimmed) {
166
+ if (history.enabled !== false && cleaned) {
143
167
  const buf = historyRef.current.items;
144
- if (buf[buf.length - 1] !== trimmed) {
145
- buf.push(trimmed);
168
+ if (buf[buf.length - 1] !== cleaned) {
169
+ buf.push(cleaned);
146
170
  if (buf.length > (history.size ?? LIMITS.composerHistorySize)) buf.shift();
147
171
  }
148
172
  historyRef.current.index = -1;
149
173
  }
150
174
  const snapshot = [...attachments];
151
- const text = value;
152
175
  reset();
153
- await onSubmit(text, snapshot);
176
+ await onSubmit(cleaned, snapshot);
154
177
  } finally {
155
178
  setIsSubmitting(false);
156
179
  }
157
- }, [value, attachments, isSubmitting, disabled, history, onSubmit, reset]);
180
+ }, [value, attachments, isSubmitting, disabled, history, onSubmit, reset, preserveExactValue]);
158
181
 
159
182
  const addAttachment = useCallback(
160
183
  (a: ChatAttachment) => {
@@ -236,8 +259,17 @@ export function useChatComposer(options: UseChatComposerOptions): UseChatCompose
236
259
  [onPasteFiles],
237
260
  );
238
261
 
262
+ // canSubmit mirrors what submit() will actually do — gate the Send
263
+ // button on the post-sanitation result so a whitespace-only draft
264
+ // renders Send as disabled (no false-hope affordance).
265
+ // preserveExactValue callers fall back to raw .trim() — they
266
+ // opted out of sanitation, but we still don't enable Send on a
267
+ // pure-whitespace draft (an empty message is rarely intentional).
239
268
  const canSubmit =
240
- !disabled && !isSubmitting && (value.trim().length > 0 || attachments.length > 0);
269
+ !disabled && !isSubmitting && (
270
+ (preserveExactValue ? value.trim().length : sanitizeDraft(value).length) > 0
271
+ || attachments.length > 0
272
+ );
241
273
 
242
274
  return {
243
275
  value,
@@ -113,6 +113,11 @@ export {
113
113
  export { useChatLightbox, type UseChatLightboxReturn, type ChatLightboxState } from './hooks';
114
114
  export { collectImageAttachments } from './utils/collectImageAttachments';
115
115
 
116
+ // Draft sanitation — trim, collapse runs, strip zero-width chars.
117
+ // Wire into your composer's submit handler / Send-button gate so
118
+ // empty-after-cleanup drafts don't fire bogus messages.
119
+ export { sanitizeDraft, isSubmittableDraft } from './utils/sanitizeDraft';
120
+
116
121
  // Dev logger (consola-based, namespace "chat:*")
117
122
  export { getChatLogger, type ChatLogger, type ChatLogScope } from './core/logger';
118
123
 
@@ -0,0 +1,72 @@
1
+ /**
2
+ * sanitizeDraft — minimal pre-submit cleanup for chat-composer drafts.
3
+ *
4
+ * Mirrors the conservative behaviour ChatGPT / Claude / Telegram
5
+ * actually ship: clean the obvious junk the user didn't intend to
6
+ * send, touch nothing that *could* be intentional.
7
+ *
8
+ * **What we DO touch:**
9
+ *
10
+ * 1. Trim leading/trailing whitespace (spaces, tabs, newlines,
11
+ * NBSP). The user typing `\n\n hello \n` meant `hello`.
12
+ * 2. Normalise line endings — `\r\n` / `\r` → `\n`. Pasted Windows
13
+ * / old-mac text gets the same internal shape, so the LLM
14
+ * tokeniser and markdown renderer see one canonical form.
15
+ * 3. Strip zero-width / invisible characters that web-paste
16
+ * smuggles in: ZWSP (U+200B), ZWNJ (U+200C), ZWJ (U+200D),
17
+ * BOM / ZWNBSP (U+FEFF). They're invisible, break LLM
18
+ * tokenisation, and the user never meant to type them.
19
+ *
20
+ * **What we DO NOT touch (and why):**
21
+ *
22
+ * - **Internal whitespace runs** (3+ spaces, tabs, blank lines).
23
+ * Code indentation depends on these. ChatGPT preserves them as
24
+ * typed — " if (x):\n return" stays four-space-indented.
25
+ * Collapsing them is the path to subtly broken code snippets.
26
+ *
27
+ * - **Bidi override marks** (U+200E LRM, U+200F RLM, U+202A..U+202E).
28
+ * Legitimately used in Arabic / Hebrew / mixed-direction text.
29
+ * Stripping silently breaks RTL users. If a specific deployment
30
+ * wants to block them as a security measure, do it at that layer
31
+ * with explicit user-visible feedback.
32
+ *
33
+ * - **Tabs vs spaces** beyond rule 1. Could be either code or
34
+ * prose; without parsing markdown we can't tell.
35
+ *
36
+ * - **Emoji, mentions, URLs, code spans** — passthrough text.
37
+ *
38
+ * The function is intentionally tiny — every rule earns its keep
39
+ * with a concrete "user pasted X from Y, got nonsense" story.
40
+ *
41
+ * **Idempotent**: `sanitizeDraft(sanitizeDraft(x)) === sanitizeDraft(x)`.
42
+ */
43
+ export function sanitizeDraft(input: string): string {
44
+ if (!input) return '';
45
+
46
+ // Strip zero-width invisibles. Done FIRST so the trim below sees
47
+ // the real content edges — a stray ZWSP at the start would
48
+ // otherwise count as non-whitespace and survive the trim.
49
+ // U+200B ZWSP, U+200C ZWNJ, U+200D ZWJ, U+FEFF BOM/ZWNBSP.
50
+ let s = input.replace(/[​‌‍]/g, '');
51
+
52
+ // Normalise line endings.
53
+ s = s.replace(/\r\n?/g, '\n');
54
+
55
+ // Trim outer whitespace (includes \n, \t, NBSP via String.trim).
56
+ s = s.trim();
57
+
58
+ return s;
59
+ }
60
+
61
+ /**
62
+ * Convenience predicate: true when the draft is non-empty AFTER
63
+ * sanitation. Use to gate Send buttons / Enter submits so an empty
64
+ * or whitespace-only draft never produces a real message.
65
+ *
66
+ * Cheaper than sanitizeDraft(input).length > 0 only marginally —
67
+ * we still allocate the cleaned string. Kept as a named helper for
68
+ * call-site clarity.
69
+ */
70
+ export function isSubmittableDraft(input: string): boolean {
71
+ return sanitizeDraft(input).length > 0;
72
+ }
@@ -13,6 +13,7 @@ import {
13
13
  } from 'lucide-react';
14
14
  import { createMentionSuggestion } from './createMentionSuggestion';
15
15
  import { mentionPresets } from './mentionPresets';
16
+ import { SubmitOnEnter } from './submitOnEnter';
16
17
  import type { MentionAttrs, MentionConfig } from './types';
17
18
  import './styles.css';
18
19
 
@@ -68,6 +69,25 @@ export interface MarkdownEditorProps {
68
69
  mentions?: MentionConfig;
69
70
  /** Called when mentioned IDs change */
70
71
  onMentionIdsChange?: (ids: string[]) => void;
72
+ /**
73
+ * Called when the user presses Enter (without Shift, no IME
74
+ * composition, no mention popover open). When set, Enter submits
75
+ * and Shift+Enter inserts a newline — ChatGPT / Telegram chat
76
+ * behaviour. When omitted, Enter behaves as Tiptap default
77
+ * (HardBreak).
78
+ *
79
+ * Implementation lives in `submitOnEnter.ts` — it's a Tiptap
80
+ * keymap extension, NOT a React wrapper handler. Wrapper-level
81
+ * onKeyDown fires AFTER ProseMirror's keymap commits HardBreak in
82
+ * the same tick; routing through the extension lets us intercept
83
+ * before HardBreak runs.
84
+ *
85
+ * Return value (optional): truthy / undefined = consume the key
86
+ * (default). Return `false` from onSubmit to let Tiptap fall
87
+ * through to HardBreak — useful for guards like "don't submit an
88
+ * empty draft".
89
+ */
90
+ onSubmit?: () => boolean | void;
71
91
  }
72
92
 
73
93
  // ── Component ──
@@ -82,7 +102,15 @@ export function MarkdownEditor({
82
102
  showToolbar = true,
83
103
  mentions,
84
104
  onMentionIdsChange,
105
+ onSubmit,
85
106
  }: MarkdownEditorProps) {
107
+ // Keep the latest onSubmit in a ref so the Tiptap extension's
108
+ // keymap closure always calls the freshest handler — Tiptap's
109
+ // useEditor initialises extensions ONCE on first render. Without
110
+ // the ref the extension would call a stale onSubmit (e.g. one
111
+ // that references an outdated `value`).
112
+ const onSubmitRef = useRef(onSubmit);
113
+ onSubmitRef.current = onSubmit;
86
114
  const isExternalUpdate = useRef(false);
87
115
 
88
116
  // ── Dev-mode trap detector ──
@@ -115,6 +143,18 @@ export function MarkdownEditor({
115
143
  StarterKit.configure({ heading: { levels: [1, 2, 3] } }),
116
144
  Placeholder.configure({ placeholder }),
117
145
  Markdown,
146
+ // SubmitOnEnter — when the consumer wired an onSubmit, intercept
147
+ // Enter at the keymap level (before StarterKit's HardBreak).
148
+ // The extension calls through `onSubmitRef.current` so handler
149
+ // identity changes don't require an editor rebuild. See
150
+ // submitOnEnter.ts for the keymap-vs-wrapper-handler rationale.
151
+ SubmitOnEnter.configure({
152
+ onSubmit: () => {
153
+ const h = onSubmitRef.current;
154
+ if (!h) return false; // no handler → let Tiptap insert HardBreak
155
+ return h();
156
+ },
157
+ }),
118
158
  ];
119
159
 
120
160
  if (mentions) {
@@ -58,6 +58,7 @@ import '@djangocfg/ui-tools/dist.css';
58
58
  | `showToolbar` | `boolean` | `true` | Show formatting toolbar |
59
59
  | `mentions` | `MentionConfig` | — | `@`-mention autocomplete config |
60
60
  | `onMentionIdsChange` | `(ids: string[]) => void` | — | Called when mentioned IDs change |
61
+ | `onSubmit` | `() => boolean \| void` | — | Enter handler — when set, Enter submits and Shift+Enter inserts a newline (ChatGPT / Telegram chat behaviour). Return `false` to fall back to default HardBreak. See [Submit on Enter](#submit-on-enter) below. |
61
62
 
62
63
  ### `MentionConfig` fields
63
64
 
@@ -142,6 +143,39 @@ Either `id` or `label` may be empty strings if upstream config didn't populate t
142
143
 
143
144
  > Mentions are write-only: the markdown isn't parsed back into mention nodes on `setContent`. After submit/reset, the editor receives a plain string — fine for chat composers.
144
145
 
146
+ ## Submit on Enter
147
+
148
+ For chat composers you usually want **Enter = send**, **Shift+Enter = newline** — ChatGPT / Telegram / Slack behaviour. Pass an `onSubmit` and that's what you get:
149
+
150
+ ```tsx
151
+ <MarkdownEditor
152
+ value={text}
153
+ onChange={setText}
154
+ onSubmit={() => {
155
+ if (!text.trim()) return false // fall back to HardBreak
156
+ void send(text)
157
+ // returning undefined (or true) consumes the key — newline isn't inserted
158
+ }}
159
+ />
160
+ ```
161
+
162
+ ### How it works (and why a Tiptap extension, not a React onKeyDown)
163
+
164
+ The implementation lives in `submitOnEnter.ts` — a Tiptap `Extension` that registers a keyboard shortcut via `addKeyboardShortcuts`. It runs **inside ProseMirror's keymap pipeline at higher priority than StarterKit's HardBreak**, so we intercept Enter **before** the hard-break transaction is dispatched.
165
+
166
+ A naïve wrapper handler (`<div onKeyDownCapture={...}>` calling `preventDefault`) does NOT work reliably: ProseMirror's keymap is a plugin inside the editor, which fires its handler in the same event tick as the React capture phase but **commits the HardBreak transaction before React's stopPropagation gets a chance to matter**. The user sees a hard-break flash in, then the next Enter submits — the "first Enter inserts newline" bug we shipped before this extension landed.
167
+
168
+ ### Behaviour details
169
+
170
+ | Key | Behaviour |
171
+ |---|---|
172
+ | `Enter` | Calls `onSubmit()`. Returns `true`/`undefined` ⇒ consume. Returns `false` ⇒ fall through to HardBreak. |
173
+ | `Shift+Enter` | Always inserts a newline. Bound to `() => false` so the chain falls through cleanly. |
174
+ | Mention popover open | Enter is given to the suggestion plugin (it picks the active item) — detected by querying `.markdown-mention-list`. |
175
+ | IME composition | Native browser composition events are not intercepted (Tiptap handles them upstream). |
176
+
177
+ The handler is captured via a ref inside `MarkdownEditor`, so swapping `onSubmit` between renders is safe — the latest closure always fires, no editor rebuild needed.
178
+
145
179
  ## Dependencies
146
180
 
147
181
  All Tiptap packages and `@floating-ui/dom` are direct dependencies — no extra installs needed.
@@ -0,0 +1,67 @@
1
+ // SubmitOnEnter — Tiptap extension that hooks ProseMirror's keymap
2
+ // before StarterKit's HardBreak so Enter consistently submits and
3
+ // Shift+Enter inserts a newline (ChatGPT / Telegram behaviour).
4
+ //
5
+ // Why an extension and not a wrapper `onKeyDown` handler:
6
+ //
7
+ // Tiptap registers its keymaps inside the ProseMirror keymap
8
+ // plugin, which runs in the ProseMirror dispatch pipeline. A React
9
+ // `onKeyDown` / `onKeyDownCapture` on a wrapper div fires AFTER
10
+ // ProseMirror has already committed the HardBreak transaction in
11
+ // the same event tick — so calling `preventDefault()` there is too
12
+ // late: the user sees a hard-break flash in, the React handler
13
+ // then runs its preventDefault, and the next Enter submits.
14
+ //
15
+ // Registering the keybinding via `addKeyboardShortcuts` puts us in
16
+ // the same keymap pipeline at a higher priority than StarterKit's
17
+ // default Enter binding, so we intercept before HardBreak runs.
18
+ //
19
+ // The handler also respects IME composition and the mention popover
20
+ // (Tiptap's suggestion plugin captures Enter when its popover is
21
+ // open — we mirror that by checking `.markdown-mention-list` in the
22
+ // DOM; same predicate the wrapper used).
23
+
24
+ import { Extension } from '@tiptap/core';
25
+
26
+ export interface SubmitOnEnterOptions {
27
+ /** Fired when Enter is pressed without Shift, no IME composition,
28
+ * and no mention popover open. Return `true` to consume the key
29
+ * (default behaviour), `false` to let ProseMirror handle it (i.e.
30
+ * fall back to HardBreak). */
31
+ onSubmit: () => boolean | void;
32
+ }
33
+
34
+ export const SubmitOnEnter = Extension.create<SubmitOnEnterOptions>({
35
+ name: 'submitOnEnter',
36
+
37
+ addOptions() {
38
+ return {
39
+ // Default no-op — explicit consumer must override. We never
40
+ // intercept Enter unless an onSubmit is wired, so leaving the
41
+ // extension installed with no handler is safe.
42
+ onSubmit: () => false,
43
+ };
44
+ },
45
+
46
+ addKeyboardShortcuts() {
47
+ return {
48
+ Enter: () => {
49
+ // Mention suggestion popover owns Enter while open.
50
+ // The suggestion plugin renders the list with this class
51
+ // (see createMentionSuggestion.ts).
52
+ if (typeof document !== 'undefined' && document.querySelector('.markdown-mention-list')) {
53
+ return false;
54
+ }
55
+ const result = this.options.onSubmit();
56
+ // Default: consume the key (true). Only let it fall through
57
+ // to HardBreak if onSubmit explicitly returned false.
58
+ return result !== false;
59
+ },
60
+ // Shift+Enter — always insert a newline. Tiptap's StarterKit
61
+ // already binds this to HardBreak; we re-bind to `false` (not
62
+ // handled) so the chain falls through cleanly even if other
63
+ // extensions try to grab Shift+Enter.
64
+ 'Shift-Enter': () => false,
65
+ };
66
+ },
67
+ });
@@ -1,14 +0,0 @@
1
- 'use strict';
2
-
3
- var chunkUVIFD3TH_cjs = require('./chunk-UVIFD3TH.cjs');
4
- require('./chunk-XACCHZH2.cjs');
5
- require('./chunk-OLISEQHS.cjs');
6
-
7
-
8
-
9
- Object.defineProperty(exports, "ChatRoot", {
10
- enumerable: true,
11
- get: function () { return chunkUVIFD3TH_cjs.ChatRoot; }
12
- });
13
- //# sourceMappingURL=ChatRoot-F5XXERXU.cjs.map
14
- //# sourceMappingURL=ChatRoot-F5XXERXU.cjs.map
@@ -1,5 +0,0 @@
1
- export { ChatRoot } from './chunk-JXBEKSNT.mjs';
2
- import './chunk-NWUT327A.mjs';
3
- import './chunk-N2XQF2OL.mjs';
4
- //# sourceMappingURL=ChatRoot-T7D7QRCH.mjs.map
5
- //# sourceMappingURL=ChatRoot-T7D7QRCH.mjs.map