@tangle-network/agent-app 0.46.3 → 0.46.4

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.
@@ -0,0 +1,36 @@
1
+ // src/web-react/mention-pill.ts
2
+ var MENTION_PILL_CLASS = "rounded-md bg-primary/10 px-1 py-0.5 font-medium text-primary";
3
+
4
+ // src/web-react/class-names.ts
5
+ function joinClasses(...parts) {
6
+ const kept = [];
7
+ for (const part of parts) {
8
+ if (typeof part !== "string") continue;
9
+ const trimmed = part.trim();
10
+ if (trimmed.length > 0) kept.push(trimmed);
11
+ }
12
+ return kept.join(" ");
13
+ }
14
+
15
+ // src/web-react/mention-boundaries.ts
16
+ var PATH_CONTINUATION_CHAR = /[\p{L}\p{M}\p{N}._\-/]/u;
17
+ var WORD_CHAR = /[\p{L}\p{M}\p{N}]/u;
18
+ function charBefore(text, index) {
19
+ if (index <= 0) return void 0;
20
+ return Array.from(text.slice(Math.max(0, index - 2), index)).pop();
21
+ }
22
+ function charAt(text, index) {
23
+ if (index >= text.length) return void 0;
24
+ const codePoint = text.codePointAt(index);
25
+ return codePoint === void 0 ? void 0 : String.fromCodePoint(codePoint);
26
+ }
27
+
28
+ export {
29
+ MENTION_PILL_CLASS,
30
+ joinClasses,
31
+ PATH_CONTINUATION_CHAR,
32
+ WORD_CHAR,
33
+ charBefore,
34
+ charAt
35
+ };
36
+ //# sourceMappingURL=chunk-RLOHIX5Y.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/web-react/mention-pill.ts","../src/web-react/class-names.ts","../src/web-react/mention-boundaries.ts"],"sourcesContent":["/**\n * Visual contract for a rendered mention pill. The TipTap extension in\n * `mention-editor.tsx` styles its inline atom node with this, and any other\n * surface that renders a resolved mention (e.g. a sent-message transcript)\n * uses the same constant so the two stay one visual contract instead of\n * silently drifting apart. Kept in its own module (no TipTap import) so\n * importing it never pulls the lazily-loaded editor chunk into a consumer's\n * bundle.\n */\nexport const MENTION_PILL_CLASS = 'rounded-md bg-primary/10 px-1 py-0.5 font-medium text-primary'\n","/**\n * One class-attribute join for this subpath.\n *\n * The pattern it replaces is `` `base ${className ?? ''}` ``, which emits\n * `class=\"base \"` for every caller that passes nothing: a trailing separator in\n * the DOM, in every snapshot, and in every assertion that compares the attribute\n * instead of searching it. Interpolating an absent value is the defect — the\n * fix is to never build the attribute by interpolation.\n */\nexport function joinClasses(...parts: ReadonlyArray<string | false | null | undefined>): string {\n const kept: string[] = []\n for (const part of parts) {\n if (typeof part !== 'string') continue\n const trimmed = part.trim()\n if (trimmed.length > 0) kept.push(trimmed)\n }\n return kept.join(' ')\n}\n","/**\n * The one boundary vocabulary for reading `@<path>` mention tokens out of\n * plain text. Two inverse readings share it — the transcript segmenter\n * (`chat-mentions.ts`) and editor-pill restore (`mention-serialize.ts`) — and\n * they must agree, or a token one accepts the other mangles. Kept OUT of the\n * `/web-react` barrel on purpose: these are internals of the grammar, not\n * public API.\n */\n\n/** A character that could plausibly continue the SAME path/filename past a\n * matched token. Without this lookahead a mention of `@a/b.md` would match\n * inside the unrelated `@a/b.md.bak` and split it mid-filename.\n *\n * Unicode-aware because the wire validator (`validateSandboxMentionPath`)\n * deliberately ALLOWS non-ASCII paths — in-box filenames are arbitrary. An\n * ASCII-only class here would accept input the readers then mangle.\n * `\\p{M}` keeps DECOMPOSED filenames whole: without it, a combining accent\n * (`a` + U+0301) reads as a boundary and splits the name after its base\n * letter. */\nexport const PATH_CONTINUATION_CHAR = /[\\p{L}\\p{M}\\p{N}._\\-/]/u\n\n/** A character that, immediately BEFORE an `@`, means the `@` is part of a\n * longer token (an email local part, a handle) rather than a mention start.\n * Unicode-aware for the same reasons, combining marks included. */\nexport const WORD_CHAR = /[\\p{L}\\p{M}\\p{N}]/u\n\n/** The full character (code point) ENDING just before `index`, or undefined\n * at the start. Indexing `text[index - 1]` hands a lone UTF-16 surrogate to\n * the Unicode-aware classes above, which then misread every astral letter —\n * boundary checks must see whole characters. `index` must be a code-point\n * boundary (both callers derive it from `@` positions and id lengths, which\n * are). */\nexport function charBefore(text: string, index: number): string | undefined {\n if (index <= 0) return undefined\n return Array.from(text.slice(Math.max(0, index - 2), index)).pop()\n}\n\n/** The full character (code point) starting at `index`, or undefined past the\n * end — same surrogate rationale and precondition as {@link charBefore}. */\nexport function charAt(text: string, index: number): string | undefined {\n if (index >= text.length) return undefined\n const codePoint = text.codePointAt(index)\n return codePoint === undefined ? undefined : String.fromCodePoint(codePoint)\n}\n"],"mappings":";AASO,IAAM,qBAAqB;;;ACA3B,SAAS,eAAe,OAAiE;AAC9F,QAAM,OAAiB,CAAC;AACxB,aAAW,QAAQ,OAAO;AACxB,QAAI,OAAO,SAAS,SAAU;AAC9B,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,QAAQ,SAAS,EAAG,MAAK,KAAK,OAAO;AAAA,EAC3C;AACA,SAAO,KAAK,KAAK,GAAG;AACtB;;;ACEO,IAAM,yBAAyB;AAK/B,IAAM,YAAY;AAQlB,SAAS,WAAW,MAAc,OAAmC;AAC1E,MAAI,SAAS,EAAG,QAAO;AACvB,SAAO,MAAM,KAAK,KAAK,MAAM,KAAK,IAAI,GAAG,QAAQ,CAAC,GAAG,KAAK,CAAC,EAAE,IAAI;AACnE;AAIO,SAAS,OAAO,MAAc,OAAmC;AACtE,MAAI,SAAS,KAAK,OAAQ,QAAO;AACjC,QAAM,YAAY,KAAK,YAAY,KAAK;AACxC,SAAO,cAAc,SAAY,SAAY,OAAO,cAAc,SAAS;AAC7E;","names":[]}
@@ -1,3 +1,10 @@
1
+ import {
2
+ PATH_CONTINUATION_CHAR,
3
+ WORD_CHAR,
4
+ charAt,
5
+ charBefore,
6
+ joinClasses
7
+ } from "./chunk-RLOHIX5Y.js";
1
8
  import {
2
9
  stepActivityFlowTrace
3
10
  } from "./chunk-FBVLEGEG.js";
@@ -1412,6 +1419,9 @@ async function streamChatTurn(opts) {
1412
1419
 
1413
1420
  // src/web-react/chat-composer.tsx
1414
1421
  import {
1422
+ Component,
1423
+ lazy as lazy2,
1424
+ Suspense as Suspense2,
1415
1425
  useCallback as useCallback3,
1416
1426
  useEffect as useEffect6,
1417
1427
  useMemo as useMemo3,
@@ -1554,6 +1564,56 @@ function useDictation({ onDictate, onError }) {
1554
1564
 
1555
1565
  // src/web-react/chat-composer.tsx
1556
1566
  import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1567
+ function createLazyMentionEditor() {
1568
+ return lazy2(() => import("./mention-editor-GVLG3W7C.js").then((m) => m.loadMentionEditor()));
1569
+ }
1570
+ var MentionEditorBoundary = class extends Component {
1571
+ state = { error: null };
1572
+ static getDerivedStateFromError(error) {
1573
+ return { error };
1574
+ }
1575
+ componentDidCatch() {
1576
+ this.props.onFailed();
1577
+ }
1578
+ render() {
1579
+ if (this.state.error === null) return this.props.children;
1580
+ const message = this.state.error instanceof Error ? this.state.error.message : String(this.state.error);
1581
+ return /* @__PURE__ */ jsxs5(
1582
+ "div",
1583
+ {
1584
+ role: "alert",
1585
+ "data-testid": "composer-mention-editor-error",
1586
+ className: "rounded-xl border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive",
1587
+ children: [
1588
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-start gap-2", children: [
1589
+ /* @__PURE__ */ jsxs5("span", { className: "min-w-0 flex-1", children: [
1590
+ "The mention input failed to load: ",
1591
+ message
1592
+ ] }),
1593
+ /* @__PURE__ */ jsx7(
1594
+ "button",
1595
+ {
1596
+ type: "button",
1597
+ "aria-label": "Retry loading the mention input",
1598
+ onClick: this.props.onRetry,
1599
+ className: "shrink-0 font-medium underline-offset-2 hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-destructive/50",
1600
+ children: "Retry"
1601
+ }
1602
+ )
1603
+ ] }),
1604
+ this.props.draft.trim() !== "" && /* @__PURE__ */ jsx7(
1605
+ "p",
1606
+ {
1607
+ "data-testid": "composer-error-held-draft",
1608
+ className: "mt-1.5 max-h-20 overflow-y-auto whitespace-pre-wrap rounded-lg border border-destructive/30 bg-card px-2 py-1 text-foreground",
1609
+ children: this.props.draft
1610
+ }
1611
+ )
1612
+ ]
1613
+ }
1614
+ );
1615
+ }
1616
+ };
1557
1617
  var IS_APPLE_PLATFORM = typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/i.test(navigator.platform);
1558
1618
  function SendGlyph({ className }) {
1559
1619
  return /* @__PURE__ */ jsx7("svg", { className, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: /* @__PURE__ */ jsx7("path", { d: "M22 2 11 13M22 2l-7 20-4-9-9-4 20-7z" }) });
@@ -1644,6 +1704,7 @@ function ChatComposer({
1644
1704
  minRows = 2,
1645
1705
  maxHeight = DEFAULT_MAX_HEIGHT,
1646
1706
  trailing,
1707
+ mention,
1647
1708
  slashCommands,
1648
1709
  onDictate,
1649
1710
  onDictateError,
@@ -1659,6 +1720,13 @@ function ChatComposer({
1659
1720
  const textRef = useRef6(text);
1660
1721
  textRef.current = text;
1661
1722
  const textareaRef = useRef6(null);
1723
+ const richFocusRef = useRef6(null);
1724
+ const registerRichFocus = useCallback3((focus) => {
1725
+ richFocusRef.current = focus;
1726
+ }, []);
1727
+ const [editorEpoch, setEditorEpoch] = useState8(0);
1728
+ const MentionEditor = useMemo3(createLazyMentionEditor, [editorEpoch]);
1729
+ const [editorFailed, setEditorFailed] = useState8(false);
1662
1730
  const fileInputRef = useRef6(null);
1663
1731
  const folderInputRef = useRef6(null);
1664
1732
  const [dragOver, setDragOver] = useState8(false);
@@ -1730,21 +1798,30 @@ function ChatComposer({
1730
1798
  const end = Math.min(pending.end, text.length);
1731
1799
  el.setSelectionRange(start, end);
1732
1800
  }, [text]);
1801
+ const mentionEnabled = mention != null;
1733
1802
  useEffect6(() => {
1734
1803
  if (!focusShortcut || disabled) return;
1735
1804
  function onKeyDown(e) {
1736
1805
  if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "l") {
1737
- e.preventDefault();
1738
- textareaRef.current?.focus();
1806
+ if (mentionEnabled) {
1807
+ const focus = richFocusRef.current;
1808
+ if (!focus) return;
1809
+ e.preventDefault();
1810
+ focus();
1811
+ } else {
1812
+ e.preventDefault();
1813
+ textareaRef.current?.focus();
1814
+ }
1739
1815
  }
1740
1816
  }
1741
1817
  document.addEventListener("keydown", onKeyDown);
1742
1818
  return () => document.removeEventListener("keydown", onKeyDown);
1743
- }, [focusShortcut, disabled]);
1819
+ }, [focusShortcut, disabled, mentionEnabled]);
1744
1820
  const sendableFiles = canSubmitAttachmentsOnly ? pendingFiles : pendingFiles.filter((f) => f.status === "ready");
1745
1821
  const hasSendable = text.trim().length > 0 || sendableFiles.length > 0;
1746
1822
  const sendBlockedByStream = isStreaming && !canSubmitWhileBusy;
1747
- const canSend = hasSendable && !sendBlockedByStream && !disabled;
1823
+ const editorInputLost = mention != null && editorFailed;
1824
+ const canSend = hasSendable && !sendBlockedByStream && !disabled && !editorInputLost;
1748
1825
  const [failedSend, setFailedSend] = useState8(null);
1749
1826
  const failSend = useCallback3(
1750
1827
  (error, draft, trimmed, parts, caret) => {
@@ -1789,7 +1866,7 @@ function ChatComposer({
1789
1866
  );
1790
1867
  const send = useCallback3(() => {
1791
1868
  const trimmed = text.trim();
1792
- if (sendBlockedByStream || disabled) return;
1869
+ if (sendBlockedByStream || disabled || editorInputLost) return;
1793
1870
  const readyFiles = pendingFiles.filter((f) => f.status === "ready");
1794
1871
  const sendable = canSubmitAttachmentsOnly ? pendingFiles : readyFiles;
1795
1872
  if (!trimmed && sendable.length === 0) return;
@@ -1809,6 +1886,7 @@ function ChatComposer({
1809
1886
  text,
1810
1887
  sendBlockedByStream,
1811
1888
  disabled,
1889
+ editorInputLost,
1812
1890
  canSubmitAttachmentsOnly,
1813
1891
  attachmentsNotReadyMessage,
1814
1892
  onSendParts,
@@ -1828,7 +1906,7 @@ function ChatComposer({
1828
1906
  const slashListId = useId();
1829
1907
  const [slashActive, setSlashActive] = useState8(0);
1830
1908
  const [slashDismissedFor, setSlashDismissedFor] = useState8(null);
1831
- const slashToken = slashCommands && slashCommands.length > 0 ? /^\/(\S*)$/.exec(text)?.[1] : void 0;
1909
+ const slashToken = !mention && slashCommands && slashCommands.length > 0 ? /^\/(\S*)$/.exec(text)?.[1] : void 0;
1832
1910
  const slashOpen = slashToken !== void 0 && text !== slashDismissedFor;
1833
1911
  const slashItems = useMemo3(
1834
1912
  () => (slashCommands ?? []).map((command) => ({
@@ -1926,11 +2004,8 @@ function ChatComposer({
1926
2004
  if (e.target.files?.length) deliverFiles(Array.from(e.target.files), e.target.files);
1927
2005
  e.target.value = "";
1928
2006
  };
1929
- const handlePaste = (e) => {
1930
- if (!onAttach) return;
1931
- const clipboardFiles = e.clipboardData?.files;
1932
- if (!clipboardFiles || clipboardFiles.length === 0) return;
1933
- e.preventDefault();
2007
+ const ingestPastedFiles = (clipboardFiles) => {
2008
+ if (!onAttach || clipboardFiles.length === 0) return false;
1934
2009
  const { files, nextIndex } = renamePastedImages(
1935
2010
  Array.from(clipboardFiles),
1936
2011
  pastedImageCount.current,
@@ -1938,6 +2013,12 @@ function ChatComposer({
1938
2013
  );
1939
2014
  pastedImageCount.current = nextIndex;
1940
2015
  deliverFiles(files, clipboardFiles);
2016
+ return true;
2017
+ };
2018
+ const handlePaste = (e) => {
2019
+ const clipboardFiles = e.clipboardData?.files;
2020
+ if (!clipboardFiles || clipboardFiles.length === 0) return;
2021
+ if (ingestPastedFiles(clipboardFiles)) e.preventDefault();
1941
2022
  };
1942
2023
  const handleFolderChange = (e) => {
1943
2024
  if (e.target.files?.length) (onAttachFolder ?? onAttach)?.(e.target.files);
@@ -1978,6 +2059,7 @@ function ChatComposer({
1978
2059
  const fileChips = pendingFiles.filter((f) => f.kind !== "folder");
1979
2060
  const showAbove = controls != null && controlsPlacement === "above";
1980
2061
  const showInline = controls != null && !showAbove;
2062
+ const inputMinHeight = minRows * LINE_HEIGHT + TEXTAREA_PADDING_Y;
1981
2063
  return /* @__PURE__ */ jsxs5(
1982
2064
  "div",
1983
2065
  {
@@ -2128,22 +2210,79 @@ function ChatComposer({
2128
2210
  "data-testid": "composer-card",
2129
2211
  className: `flex flex-col gap-1.5 rounded-2xl border border-card-edge bg-card px-3 py-2.5 transition focus-within:border-primary/40 focus-within:ring-2 focus-within:ring-primary/15 ${floating ? "shadow-raised" : ""}`,
2130
2212
  children: [
2131
- /* @__PURE__ */ jsx7(
2132
- "textarea",
2133
- {
2134
- ref: textareaRef,
2135
- value: text,
2136
- onChange: (e) => setText(e.target.value),
2137
- onKeyDown: handleKeyDown,
2138
- onPaste: onAttach ? handlePaste : void 0,
2139
- placeholder,
2140
- disabled,
2141
- autoFocus,
2142
- rows: minRows,
2143
- style: { minHeight: minRows * LINE_HEIGHT + TEXTAREA_PADDING_Y, maxHeight },
2144
- "aria-label": "Message input",
2145
- className: "w-full resize-none bg-transparent px-1.5 py-1 text-base leading-6 text-foreground outline-none placeholder:text-muted-foreground disabled:opacity-50"
2146
- }
2213
+ mention ? (
2214
+ // The editor arrives as a lazy chunk; until it lands, a read-only
2215
+ // textarea with the same metrics holds the layout so the card
2216
+ // doesn't jump. The boundary contains a failed load (e.g. the
2217
+ // missing-peer error) to the input area instead of unmounting the
2218
+ // host's region.
2219
+ /* @__PURE__ */ jsx7(
2220
+ MentionEditorBoundary,
2221
+ {
2222
+ onRetry: () => {
2223
+ setEditorFailed(false);
2224
+ setEditorEpoch((epoch) => epoch + 1);
2225
+ },
2226
+ onFailed: () => setEditorFailed(true),
2227
+ draft: text,
2228
+ children: /* @__PURE__ */ jsx7(
2229
+ Suspense2,
2230
+ {
2231
+ fallback: /* @__PURE__ */ jsx7(
2232
+ "textarea",
2233
+ {
2234
+ rows: minRows,
2235
+ value: text,
2236
+ readOnly: true,
2237
+ disabled: true,
2238
+ placeholder,
2239
+ "aria-label": "Message input",
2240
+ style: { minHeight: inputMinHeight, maxHeight },
2241
+ className: "w-full resize-none bg-transparent px-1.5 py-1 text-base leading-6 text-foreground outline-none placeholder:text-muted-foreground disabled:opacity-50"
2242
+ }
2243
+ ),
2244
+ children: /* @__PURE__ */ jsx7(
2245
+ MentionEditor,
2246
+ {
2247
+ value: text,
2248
+ onChange: setText,
2249
+ onSubmit: send,
2250
+ placeholder,
2251
+ disabled,
2252
+ autoFocus,
2253
+ minHeight: inputMinHeight,
2254
+ maxHeight,
2255
+ mention,
2256
+ registerFocus: registerRichFocus,
2257
+ onPasteFiles: onAttach ? ingestPastedFiles : void 0
2258
+ }
2259
+ )
2260
+ }
2261
+ )
2262
+ },
2263
+ editorEpoch
2264
+ )
2265
+ ) : (
2266
+ // Focus: `outline-none` is safe because the card above draws the
2267
+ // keyboard indicator through `focus-within:` — one ring for
2268
+ // whichever input mode is mounted.
2269
+ /* @__PURE__ */ jsx7(
2270
+ "textarea",
2271
+ {
2272
+ ref: textareaRef,
2273
+ value: text,
2274
+ onChange: (e) => setText(e.target.value),
2275
+ onKeyDown: handleKeyDown,
2276
+ onPaste: onAttach ? handlePaste : void 0,
2277
+ placeholder,
2278
+ disabled,
2279
+ autoFocus,
2280
+ rows: minRows,
2281
+ style: { minHeight: inputMinHeight, maxHeight },
2282
+ "aria-label": "Message input",
2283
+ className: "w-full resize-none bg-transparent px-1.5 py-1 text-base leading-6 text-foreground outline-none placeholder:text-muted-foreground disabled:opacity-50"
2284
+ }
2285
+ )
2147
2286
  ),
2148
2287
  /* @__PURE__ */ jsxs5("div", { className: "flex items-end gap-2", children: [
2149
2288
  onAttach && /* @__PURE__ */ jsxs5(Fragment2, { children: [
@@ -2821,8 +2960,6 @@ function useFileMentions(options) {
2821
2960
  }
2822
2961
 
2823
2962
  // src/web-react/chat-mentions.ts
2824
- var PATH_CONTINUATION_CHAR = /[\p{L}\p{N}._\-/]/u;
2825
- var WORD_CHAR = /[\p{L}\p{N}]/u;
2826
2963
  function segmentMentionContent(content, parts) {
2827
2964
  const matched = /* @__PURE__ */ new Set();
2828
2965
  if (!content) return { segments: [], matched };
@@ -2836,7 +2973,7 @@ function segmentMentionContent(content, parts) {
2836
2973
  cursor += 1;
2837
2974
  continue;
2838
2975
  }
2839
- const prevChar = cursor > 0 ? content[cursor - 1] : void 0;
2976
+ const prevChar = charBefore(content, cursor);
2840
2977
  if (prevChar && WORD_CHAR.test(prevChar)) {
2841
2978
  cursor += 1;
2842
2979
  continue;
@@ -2847,7 +2984,7 @@ function segmentMentionContent(content, parts) {
2847
2984
  continue;
2848
2985
  }
2849
2986
  const endIdx = cursor + candidate.token.length;
2850
- const nextChar = endIdx < content.length ? content[endIdx] : void 0;
2987
+ const nextChar = charAt(content, endIdx);
2851
2988
  if (nextChar && PATH_CONTINUATION_CHAR.test(nextChar)) {
2852
2989
  cursor += 1;
2853
2990
  continue;
@@ -5342,17 +5479,6 @@ function CommandPalette({
5342
5479
  );
5343
5480
  }
5344
5481
 
5345
- // src/web-react/class-names.ts
5346
- function joinClasses(...parts) {
5347
- const kept = [];
5348
- for (const part of parts) {
5349
- if (typeof part !== "string") continue;
5350
- const trimmed = part.trim();
5351
- if (trimmed.length > 0) kept.push(trimmed);
5352
- }
5353
- return kept.join(" ");
5354
- }
5355
-
5356
5482
  // src/web-react/sparkline.tsx
5357
5483
  import { jsx as jsx13, jsxs as jsxs11 } from "react/jsx-runtime";
5358
5484
  var DEFAULT_SPARKLINE_WIDTH = 96;
@@ -6885,4 +7011,4 @@ export {
6885
7011
  useThinkingSeconds,
6886
7012
  ChatMessages
6887
7013
  };
6888
- //# sourceMappingURL=chunk-RZ6MYWMJ.js.map
7014
+ //# sourceMappingURL=chunk-YTEUZXX2.js.map