pilotswarm 0.5.27 → 0.5.29

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 (71) hide show
  1. package/package.json +1 -1
  2. package/ui/core/src/controller.js +17 -2
  3. package/ui/core/src/formatting.js +60 -16
  4. package/ui/core/src/selectors.js +213 -7
  5. package/ui/react/src/web-app.js +101 -37
  6. package/web/dist/assets/{abnfDiagram-VRR7QNED-DOZGtiQC.js → abnfDiagram-VRR7QNED-oWxPJ_GQ.js} +1 -1
  7. package/web/dist/assets/{arc-CNT314TJ.js → arc-NgTDsu-Q.js} +1 -1
  8. package/web/dist/assets/{architectureDiagram-ZJ3FMSHR-Cmi5R1em.js → architectureDiagram-ZJ3FMSHR-Bm46i-2Z.js} +1 -1
  9. package/web/dist/assets/{blockDiagram-677ZJIJ3-CFQt6Pnc.js → blockDiagram-677ZJIJ3-qBRJ9ozp.js} +1 -1
  10. package/web/dist/assets/{c4Diagram-LMCZKHZV-v7-eGb6Y.js → c4Diagram-LMCZKHZV-BD6luJ9R.js} +1 -1
  11. package/web/dist/assets/channel-CHWKMkky.js +1 -0
  12. package/web/dist/assets/{chunk-2Q5K7J3B-DoLSc3k6.js → chunk-2Q5K7J3B-CYFnGP5X.js} +1 -1
  13. package/web/dist/assets/{chunk-32BRIVSS-l-i2NvX3.js → chunk-32BRIVSS-C0VzwlaC.js} +1 -1
  14. package/web/dist/assets/{chunk-5VM5RSS4-B3s5kXyR.js → chunk-5VM5RSS4-DC6-hzz3.js} +1 -1
  15. package/web/dist/assets/{chunk-EX3LRPZG-Ckctm_wZ.js → chunk-EX3LRPZG-_VC40n49.js} +1 -1
  16. package/web/dist/assets/{chunk-JWPE2WC7-D5reV4oK.js → chunk-JWPE2WC7-BuP_EUE9.js} +1 -1
  17. package/web/dist/assets/{chunk-MOJQB5TN-B-bgmTfa.js → chunk-MOJQB5TN-D-6WLgUh.js} +1 -1
  18. package/web/dist/assets/{chunk-RYQCIY6F-CLliUW3T.js → chunk-RYQCIY6F-COg4nSHg.js} +1 -1
  19. package/web/dist/assets/{chunk-V7JOEXUC-CsAyO1PL.js → chunk-V7JOEXUC-DXKqt6ej.js} +1 -1
  20. package/web/dist/assets/{chunk-VR4S4FIN-Df6VMLs3.js → chunk-VR4S4FIN-DMEBmVN_.js} +1 -1
  21. package/web/dist/assets/{chunk-XXDRQBXY-pOkdkM00.js → chunk-XXDRQBXY-Bv9ogVHT.js} +1 -1
  22. package/web/dist/assets/classDiagram-OUVF2IWQ-mrBj2HBz.js +1 -0
  23. package/web/dist/assets/classDiagram-v2-EOCWNBFH-mrBj2HBz.js +1 -0
  24. package/web/dist/assets/{cose-bilkent-JH36ORCC-DF9LzGIn.js → cose-bilkent-JH36ORCC-D32d7zQr.js} +1 -1
  25. package/web/dist/assets/{cynefin-VYW2F7L2-COwlV5eq.js → cynefin-VYW2F7L2-7QQpYsvS.js} +1 -1
  26. package/web/dist/assets/{cynefinDiagram-TSTJHNR4-Cmf8ISYO.js → cynefinDiagram-TSTJHNR4-ByQPFpot.js} +1 -1
  27. package/web/dist/assets/{dagre-VKFMJZFB-DanZ5sUs.js → dagre-VKFMJZFB-CMo4a9uC.js} +1 -1
  28. package/web/dist/assets/{diagram-FQU43EPY-xJWijzy7.js → diagram-FQU43EPY-exMbxW0B.js} +1 -1
  29. package/web/dist/assets/{diagram-G47NLZAW-C2V1yOvV.js → diagram-G47NLZAW-CjB1mUZ3.js} +1 -1
  30. package/web/dist/assets/{diagram-NH7WQ7WH-C8SrNQ_i.js → diagram-NH7WQ7WH-B1hkOsaZ.js} +1 -1
  31. package/web/dist/assets/{diagram-OA4YK3LP-BATn2U0p.js → diagram-OA4YK3LP-C8vwEcEA.js} +1 -1
  32. package/web/dist/assets/{diagram-WEI45ONY-Dk1xkd8r.js → diagram-WEI45ONY-CoxrJWum.js} +1 -1
  33. package/web/dist/assets/{ebnfDiagram-CCIWWBDH-JFfByguj.js → ebnfDiagram-CCIWWBDH-Dzyabvsk.js} +1 -1
  34. package/web/dist/assets/{erDiagram-Q63AITRT-DnlU9tGx.js → erDiagram-Q63AITRT-DoGWkX5g.js} +1 -1
  35. package/web/dist/assets/{flowDiagram-23GEKE2U-Dnq_XrQ8.js → flowDiagram-23GEKE2U-lxptzQmL.js} +1 -1
  36. package/web/dist/assets/{ganttDiagram-NO4QXBWP-MzgQ4Gkh.js → ganttDiagram-NO4QXBWP-B0csrlon.js} +1 -1
  37. package/web/dist/assets/{gitGraphDiagram-IHSO6WYX-C2LwEO5a.js → gitGraphDiagram-IHSO6WYX-RKmWX3zj.js} +1 -1
  38. package/web/dist/assets/index-BGWTu1KK.css +1 -0
  39. package/web/dist/assets/{index-BkjhSyx0.js → index-Dt7mxgM0.js} +2 -2
  40. package/web/dist/assets/{infoDiagram-FWYZ7A6U-UMiiYK4p.js → infoDiagram-FWYZ7A6U-CNMCniEk.js} +1 -1
  41. package/web/dist/assets/{ishikawaDiagram-FXEZZL3T-CtzROVDb.js → ishikawaDiagram-FXEZZL3T-RRg8kby0.js} +1 -1
  42. package/web/dist/assets/{journeyDiagram-5HDEW3XC-C5UU06Ip.js → journeyDiagram-5HDEW3XC-qnpsaIv9.js} +1 -1
  43. package/web/dist/assets/{kanban-definition-HUTT4EX6-DYwJ4PpM.js → kanban-definition-HUTT4EX6-DIIuRFeZ.js} +1 -1
  44. package/web/dist/assets/{linear-Dg7W_aPc.js → linear-CJEXBQLu.js} +1 -1
  45. package/web/dist/assets/{mermaid.core-OUiCmXKB.js → mermaid.core-Dl9SNFGg.js} +4 -4
  46. package/web/dist/assets/{mindmap-definition-LN4V7U3C-ffFo5mVE.js → mindmap-definition-LN4V7U3C-BQc4pRs9.js} +1 -1
  47. package/web/dist/assets/{papaparse.min-DPfbFiel.js → papaparse.min-Bgg7cz2c.js} +1 -1
  48. package/web/dist/assets/{pegDiagram-2B236MQR-DJ0r7gkW.js → pegDiagram-2B236MQR-Dwv82H9E.js} +1 -1
  49. package/web/dist/assets/{pieDiagram-ENE6RG2P-BnpWDtAd.js → pieDiagram-ENE6RG2P-ia4A5QN1.js} +1 -1
  50. package/web/dist/assets/{pilotswarm-CU6jtK4w.js → pilotswarm-Bu5uSolS.js} +59 -59
  51. package/web/dist/assets/{quadrantDiagram-ABIIQ3AL-Bcxo8lQJ.js → quadrantDiagram-ABIIQ3AL-CVHA0Wcb.js} +1 -1
  52. package/web/dist/assets/{railroadDiagram-RFXS5EU6-YtDm2Hhr.js → railroadDiagram-RFXS5EU6-sr_WWsF2.js} +1 -1
  53. package/web/dist/assets/{requirementDiagram-TGXJPOKE-DIIFSM-x.js → requirementDiagram-TGXJPOKE-BUbasOTO.js} +1 -1
  54. package/web/dist/assets/{sankeyDiagram-HTMAVEWB-BH3EkrzA.js → sankeyDiagram-HTMAVEWB-BLZieRyV.js} +1 -1
  55. package/web/dist/assets/{sequenceDiagram-DBY2YBRQ-DjD5j0HL.js → sequenceDiagram-DBY2YBRQ-9QjAhkU0.js} +1 -1
  56. package/web/dist/assets/{sizeCapture-X5ZJPWSS-L5FPJ9w8.js → sizeCapture-X5ZJPWSS-C7L0QsRr.js} +1 -1
  57. package/web/dist/assets/{stateDiagram-2N3HPSRC-o-MgnF-b.js → stateDiagram-2N3HPSRC-DQieYhBl.js} +1 -1
  58. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-CaGh4nBi.js +1 -0
  59. package/web/dist/assets/{swimlanes-5IMT3BWC-B_7FbL2G.js → swimlanes-5IMT3BWC-Dyp3aLdb.js} +2 -2
  60. package/web/dist/assets/swimlanesDiagram-G3AALYLV-BpaUuDlv.js +8 -0
  61. package/web/dist/assets/{timeline-definition-FHXFAJF6-C8qUL6LV.js → timeline-definition-FHXFAJF6-DREq8pH8.js} +1 -1
  62. package/web/dist/assets/{vennDiagram-L72KCM5P-wS-9VnAs.js → vennDiagram-L72KCM5P-bRHXAhI3.js} +1 -1
  63. package/web/dist/assets/{wardleyDiagram-EHGQE667-Cz8fglhX.js → wardleyDiagram-EHGQE667-Dw2Z1Dt9.js} +1 -1
  64. package/web/dist/assets/{xychartDiagram-FW5EYKEG-CmsR-UfQ.js → xychartDiagram-FW5EYKEG-DEp4EjUc.js} +1 -1
  65. package/web/dist/index.html +3 -3
  66. package/web/dist/assets/channel-DHDU75Fc.js +0 -1
  67. package/web/dist/assets/classDiagram-OUVF2IWQ-DkWfQ9TO.js +0 -1
  68. package/web/dist/assets/classDiagram-v2-EOCWNBFH-DkWfQ9TO.js +0 -1
  69. package/web/dist/assets/index-DyL_-2Du.css +0 -1
  70. package/web/dist/assets/stateDiagram-v2-6OUMAXLB-DkGZm6xe.js +0 -1
  71. package/web/dist/assets/swimlanesDiagram-G3AALYLV-D9vnXGSI.js +0 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pilotswarm",
3
- "version": "0.5.27",
3
+ "version": "0.5.29",
4
4
  "description": "PilotSwarm application package: terminal UI, browser portal + Web API server, and MCP server \u2014 one install, three bins.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -935,7 +935,12 @@ function movePromptCursorVertically(prompt, cursor, direction) {
935
935
  return getPromptCursorIndex(prompt, targetLine, position.column);
936
936
  }
937
937
 
938
- const AUTO_HISTORY_EVENT_SOFT_CAP = 3_000;
938
+ // Automatic scroll-up expansion stops here; past it the user must ask for more
939
+ // explicitly. Exported so a surface can SHOW that control — the portal had no
940
+ // way to trigger EXPAND_HISTORY at all, so on a busy session (tens of thousands
941
+ // of events) scrolling up simply died with a status line telling the user to
942
+ // press a key that only exists in the TUI.
943
+ export const AUTO_HISTORY_EVENT_SOFT_CAP = 3_000;
939
944
  const INSPECTOR_BOTTOM_ANCHORED_TABS = new Set(["logs", "sequence"]);
940
945
  const FILE_PREVIEW_CHAR_LIMIT = 200_000;
941
946
  const MARKDOWN_FILE_EXTENSIONS = new Set([
@@ -6328,7 +6333,17 @@ export class PilotSwarmUiController {
6328
6333
 
6329
6334
  await this.maybeAutoExpandActiveHistory(requestedScrollOffset, {
6330
6335
  pages: AUTO_HISTORY_SCROLL_PAGE_COUNT,
6331
- force: Boolean(options.force),
6336
+ // Always bypass the offset gate. Every caller of this method fires
6337
+ // only when the DOM scroller is already AT the top — a direct
6338
+ // measurement. The gate is a second, weaker guess that compares a
6339
+ // DOM-derived offset against `selectChatLines` metrics, i.e. the
6340
+ // TERMINAL wrapped-line count, which the rich renderer does not
6341
+ // use. On a session whose messages are long (tables, code blocks)
6342
+ // the terminal line count balloons far past the rich DOM's scroll
6343
+ // extent, so the gate could never open and older history was
6344
+ // unreachable — while a session of short messages worked fine.
6345
+ // The arm/fire handshake above still debounces the wheel path.
6346
+ force: true,
6332
6347
  });
6333
6348
  }
6334
6349
 
@@ -100,6 +100,58 @@ export function shortModelName(model) {
100
100
  return value.includes(":") ? value.split(":").slice(1).join(":") : value;
101
101
  }
102
102
 
103
+ /*
104
+ * `date.toLocaleTimeString(locale, opts)` builds a fresh Intl.DateTimeFormat on
105
+ * EVERY call. Constructing the formatter is the expensive part — the formatting
106
+ * itself is cheap — and these timestamps are re-derived for every row on every
107
+ * render, so a pane resize paid for thousands of throwaway formatters per drag.
108
+ * Measured on the chk portal: this one function was 5,332ms of self time in a
109
+ * 5,800ms drag profile (91.8%), with nothing else above 60ms.
110
+ *
111
+ * Built lazily rather than at module load: callers that never format a
112
+ * timestamp (the CLI paths) should not pay ICU initialisation on import.
113
+ *
114
+ * Passing `undefined` as the locale keeps the user's own locale, matching what
115
+ * toLocaleTimeString did. The "en-GB" ones are deliberately fixed — the compact
116
+ * form is a fixed-width column and a locale-dependent month name would break it.
117
+ */
118
+ let hourMinuteFormatter = null;
119
+ let hourMinuteSecondFormatter = null;
120
+ let monthShortFormatter = null;
121
+ let monthDayFormatter = null;
122
+
123
+ function hourMinuteFormat() {
124
+ if (!hourMinuteFormatter) {
125
+ hourMinuteFormatter = new Intl.DateTimeFormat(undefined, {
126
+ hour: "2-digit", minute: "2-digit", hour12: false,
127
+ });
128
+ }
129
+ return hourMinuteFormatter;
130
+ }
131
+
132
+ function hourMinuteSecondFormat() {
133
+ if (!hourMinuteSecondFormatter) {
134
+ hourMinuteSecondFormatter = new Intl.DateTimeFormat(undefined, {
135
+ hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false,
136
+ });
137
+ }
138
+ return hourMinuteSecondFormatter;
139
+ }
140
+
141
+ function monthShortFormat() {
142
+ if (!monthShortFormatter) {
143
+ monthShortFormatter = new Intl.DateTimeFormat("en-GB", { month: "short" });
144
+ }
145
+ return monthShortFormatter;
146
+ }
147
+
148
+ function monthDayFormat() {
149
+ if (!monthDayFormatter) {
150
+ monthDayFormatter = new Intl.DateTimeFormat("en-GB", { month: "short", day: "numeric" });
151
+ }
152
+ return monthDayFormatter;
153
+ }
154
+
103
155
  /**
104
156
  * Compact timestamp for fixed-width columns (the sequence pane).
105
157
  *
@@ -114,19 +166,16 @@ export function formatTimestampCompact(value, now = new Date()) {
114
166
  try {
115
167
  const date = value instanceof Date ? value : new Date(value);
116
168
  if (Number.isNaN(date.getTime())) return "";
117
- const hhmm = date.toLocaleTimeString(undefined, {
118
- hour: "2-digit", minute: "2-digit", hour12: false,
119
- });
120
169
  const sameLocalDay = date.getFullYear() === now.getFullYear()
121
170
  && date.getMonth() === now.getMonth()
122
171
  && date.getDate() === now.getDate();
123
- if (sameLocalDay) {
124
- return date.toLocaleTimeString(undefined, {
125
- hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false,
126
- });
127
- }
172
+ // Same-day returns a DIFFERENT string (it carries seconds), so
173
+ // computing hhmm before this branch built a formatted time only to
174
+ // throw it away on the most common row in the list.
175
+ if (sameLocalDay) return hourMinuteSecondFormat().format(date);
176
+ const hhmm = hourMinuteFormat().format(date);
128
177
  const day = String(date.getDate());
129
- const month = date.toLocaleDateString("en-GB", { month: "short" });
178
+ const month = monthShortFormat().format(date);
130
179
  // Year only when it differs — a two-digit suffix, since the column is
131
180
  // shared with same-day rows and every extra char costs lane width.
132
181
  const year = date.getFullYear() === now.getFullYear()
@@ -142,19 +191,14 @@ export function formatTimestamp(value, now = new Date()) {
142
191
  if (!value) return "";
143
192
  try {
144
193
  const date = value instanceof Date ? value : new Date(value);
145
- const time = date.toLocaleTimeString(undefined, {
146
- hour: "2-digit",
147
- minute: "2-digit",
148
- second: "2-digit",
149
- hour12: false,
150
- });
194
+ const time = hourMinuteSecondFormat().format(date);
151
195
  // Same-day messages show time only; anything older carries its date
152
196
  // so a transcript spanning days stays unambiguous.
153
197
  const sameLocalDay = date.getFullYear() === now.getFullYear()
154
198
  && date.getMonth() === now.getMonth()
155
199
  && date.getDate() === now.getDate();
156
200
  if (sameLocalDay) return time;
157
- const day = date.toLocaleDateString("en-GB", { month: "short", day: "numeric" });
201
+ const day = monthDayFormat().format(date);
158
202
  const year = date.getFullYear() === now.getFullYear() ? "" : ` ${date.getFullYear()}`;
159
203
  return `${day}${year} ${time}`;
160
204
  } catch {
@@ -892,6 +892,12 @@ export function selectSessionRows(state) {
892
892
  entry, session, active, pinned, selected, query, ownerFilter, auth,
893
893
  state.connection?.mode, state.sessions.selectMode,
894
894
  totalDescendantCounts, visibleDescendantCounts,
895
+ // The root system row renders as the branding title, and this
896
+ // selector is called with two different state shapes (the portal
897
+ // builds a synthetic one). Without this the cache could hand a row
898
+ // built under one branding to a caller using another, and the root
899
+ // session's name would flip between them.
900
+ state.branding?.title,
895
901
  ];
896
902
  const cached = memo.get(entry.sessionId);
897
903
  if (cached && sameRowDeps(cached.deps, deps)) return cached.row;
@@ -2402,10 +2408,75 @@ export function selectLiveActivityLines(state, options = {}) {
2402
2408
  return [fitRuns(runs, maxWidth)];
2403
2409
  }
2404
2410
 
2411
+ // Recent selectChatLines results, compared by the IDENTITY of every input.
2412
+ //
2413
+ // This wraps the entire transcript to terminal lines, and it runs from
2414
+ // scrollPane / scrollPaneTo / the visual-offset helpers — i.e. on every scroll
2415
+ // action, to answer nothing more than "how many lines are there". Measured
2416
+ // 1.6ms at 300 messages and 5.1ms at 1500, per scroll.
2417
+ //
2418
+ // Keyed on identities rather than fields on purpose: selectActiveChat is NOT a
2419
+ // pure function of the chat array — it also appends a pending-question message,
2420
+ // an answered-question message and a session-error message, and branches on
2421
+ // ui.chatViewMode and branding. Anything derived from `session` therefore has
2422
+ // to invalidate when `session` does, which identity comparison gives for free.
2423
+ //
2424
+ // `byId` is in the key for two reasons found by adversarial review, both of
2425
+ // which produced visibly wrong output:
2426
+ //
2427
+ // 1. For a GROUP session, selectActiveChat ignores the group's own history
2428
+ // entirely and builds a Members table by scanning every OTHER session in
2429
+ // byId (title / status / shortSummary). Renaming a member changes none of
2430
+ // the group row's own aggregate counters, and the reducer PRESERVES a
2431
+ // session object's identity when nothing on it moved — so without byId the
2432
+ // table froze on the old titles indefinitely. (An earlier version of this
2433
+ // comment claimed the poll rebuilds the session object every time. It does
2434
+ // not, and that wrong assumption is what made the bug possible.)
2435
+ // 2. This memo is module-level and shared by callers that pass DIFFERENT
2436
+ // state shapes: the TUI's ChatPane passes a synthetic single-session byId
2437
+ // while the controller's scroll math passes the real full state. Every
2438
+ // other key field can match between them, so whichever missed first
2439
+ // populated one entry for both, and the member list flipped depending on
2440
+ // call order. Distinct byId objects now key them apart.
2441
+ const CHAT_LINES_MEMO_MAX = 4;
2442
+ let chatLinesMemo = [];
2443
+
2405
2444
  export function selectChatLines(state, maxWidth = 80, options = {}) {
2445
+ const memoSessionId = state?.sessions?.activeSessionId ?? null;
2446
+ const memoKey = {
2447
+ sessionId: memoSessionId,
2448
+ session: memoSessionId ? state?.sessions?.byId?.[memoSessionId] ?? null : null,
2449
+ byId: state?.sessions?.byId ?? null,
2450
+ chat: state?.history?.bySessionId?.get?.(memoSessionId)?.chat ?? null,
2451
+ viewMode: state?.ui?.chatViewMode ?? null,
2452
+ branding: state?.branding ?? null,
2453
+ principal: state?.auth?.principal ?? null,
2454
+ maxWidth,
2455
+ tableMode: options?.tableMode ?? null,
2456
+ };
2457
+ for (const entry of chatLinesMemo) {
2458
+ if (entry.sessionId === memoKey.sessionId
2459
+ && entry.session === memoKey.session
2460
+ && entry.byId === memoKey.byId
2461
+ && entry.chat === memoKey.chat
2462
+ && entry.viewMode === memoKey.viewMode
2463
+ && entry.branding === memoKey.branding
2464
+ && entry.principal === memoKey.principal
2465
+ && Object.is(entry.maxWidth, memoKey.maxWidth)
2466
+ && entry.tableMode === memoKey.tableMode) {
2467
+ return entry.lines;
2468
+ }
2469
+ }
2470
+
2471
+ const memoize = (lines) => {
2472
+ chatLinesMemo.unshift({ ...memoKey, lines });
2473
+ if (chatLinesMemo.length > CHAT_LINES_MEMO_MAX) chatLinesMemo.length = CHAT_LINES_MEMO_MAX;
2474
+ return lines;
2475
+ };
2476
+
2406
2477
  const messages = selectActiveChat(state);
2407
2478
  if (!messages || messages.length === 0) {
2408
- return [{ text: "No messages yet.", color: "gray" }];
2479
+ return memoize([{ text: "No messages yet.", color: "gray" }]);
2409
2480
  }
2410
2481
 
2411
2482
  // The current viewer's identity key — a user.message whose sender differs
@@ -2449,7 +2520,7 @@ export function selectChatLines(state, maxWidth = 80, options = {}) {
2449
2520
  lines.push(createBlankLine());
2450
2521
  }
2451
2522
  }
2452
- return lines.length > 0 ? lines : [{ text: "No messages yet.", color: "gray" }];
2523
+ return memoize(lines.length > 0 ? lines : [{ text: "No messages yet.", color: "gray" }]);
2453
2524
  }
2454
2525
 
2455
2526
  // True when a chat message is plain user/assistant prose the rich (block)
@@ -2486,6 +2557,10 @@ function isRichRenderableChatMessage(message) {
2486
2557
  *
2487
2558
  * The TUI never calls this; selectChatLines remains the terminal path.
2488
2559
  */
2560
+ // Rich message blocks, cached on the message object. Keyed weakly so entries
2561
+ // die with the transcript they came from.
2562
+ const chatMessageBlockCache = new WeakMap();
2563
+
2489
2564
  export function selectChatBlocks(state, maxWidth = 80, options = {}) {
2490
2565
  const messages = selectActiveChat(state);
2491
2566
  if (!messages || messages.length === 0) return [];
@@ -2507,6 +2582,41 @@ export function selectChatBlocks(state, maxWidth = 80, options = {}) {
2507
2582
 
2508
2583
  const blocks = [];
2509
2584
  for (const message of messages) {
2585
+ // Fast path FIRST. Classifying a message as rich-renderable scans its
2586
+ // text (asked-and-answered parsing, system-notice splitting), and that
2587
+ // ran for every message on every call — so a pane-splitter drag paid
2588
+ // it across the whole transcript per width step. A cache entry only
2589
+ // exists for a message already classified rich, and `kind` is part of
2590
+ // what is compared, so a hit is safe to take before classifying.
2591
+ // Object.is, NOT ===: `createdAt` is NaN whenever the source event had
2592
+ // no parseable timestamp, and NaN === NaN is false, so a plain equality
2593
+ // check could never hit for those messages — the cache would silently
2594
+ // do nothing while still costing a lookup. Same trap as comparing
2595
+ // Infinity by subtraction.
2596
+ const cached = message ? chatMessageBlockCache.get(message) : null;
2597
+ if (cached
2598
+ && Object.is(cached.text, message.text)
2599
+ && Object.is(cached.kind, message.kind)
2600
+ && Object.is(cached.role, message.role)
2601
+ && Object.is(cached.id, message.id)
2602
+ && Object.is(cached.pendingPhase, message.pendingPhase)
2603
+ && Object.is(cached.stopped, message.stopped)
2604
+ // The header is derived from these, and the chips from attachments.
2605
+ // Messages are mutated in place (pendingPhase provably is), so every
2606
+ // field the block is built from has to be compared — not just the
2607
+ // obvious ones.
2608
+ && Object.is(cached.createdAt, message.createdAt)
2609
+ && Object.is(cached.time, message.time)
2610
+ && Object.is(cached.sender, message.sender)
2611
+ && Object.is(cached.attachments, message.attachments)
2612
+ && Object.is(cached.tableMode, buildOptions.tableMode)
2613
+ && Object.is(cached.viewerKey, buildOptions.viewerKey)
2614
+ && Object.is(cached.ownerKey, buildOptions.ownerKey)
2615
+ && Object.is(cached.sharedContext, buildOptions.sharedContext)) {
2616
+ blocks.push(cached.block);
2617
+ continue;
2618
+ }
2619
+
2510
2620
  if (message?.kind === "epoch-divider") {
2511
2621
  blocks.push({ kind: "lines", variant: "divider", lines: [buildEpochDividerLine(message, maxWidth)] });
2512
2622
  continue;
@@ -2520,9 +2630,15 @@ export function selectChatBlocks(state, maxWidth = 80, options = {}) {
2520
2630
  continue;
2521
2631
  }
2522
2632
  if (isRichRenderableChatMessage(message)) {
2633
+ // A rich message block does NOT depend on maxWidth — the browser
2634
+ // wraps this text with CSS; only the terminal needs a column count.
2635
+ // Rebuilding it on every width step meant dragging a pane splitter
2636
+ // re-parsed artifact links and re-derived headers for the entire
2637
+ // loaded transcript to produce byte-identical output. Cache on the
2638
+ // message itself, keyed by the inputs that CAN change it.
2523
2639
  const attachmentLines = buildAttachmentChipLines(message, { tableMode: "sentinel" });
2524
2640
  const attachmentSentinel = attachmentLines.find((line) => line?.kind === "imageAttachments") || null;
2525
- blocks.push({
2641
+ const block = {
2526
2642
  kind: "message",
2527
2643
  id: message?.id ?? null,
2528
2644
  role: message.role,
@@ -2532,17 +2648,107 @@ export function selectChatBlocks(state, maxWidth = 80, options = {}) {
2532
2648
  attachments: attachmentSentinel
2533
2649
  ? { sessionId: attachmentSentinel.sessionId, attachments: attachmentSentinel.attachments }
2534
2650
  : null,
2651
+ };
2652
+ chatMessageBlockCache.set(message, {
2653
+ block,
2654
+ text: message.text,
2655
+ kind: message.kind,
2656
+ createdAt: message.createdAt,
2657
+ time: message.time,
2658
+ sender: message.sender,
2659
+ attachments: message.attachments,
2660
+ role: message.role,
2661
+ id: message.id,
2662
+ pendingPhase: message.pendingPhase,
2663
+ stopped: message.stopped,
2664
+ tableMode: buildOptions.tableMode,
2665
+ viewerKey: buildOptions.viewerKey,
2666
+ ownerKey: buildOptions.ownerKey,
2667
+ sharedContext: buildOptions.sharedContext,
2535
2668
  });
2669
+ blocks.push(block);
2536
2670
  continue;
2537
2671
  }
2538
- const lines = buildChatMessageLines(message, maxWidth, buildOptions);
2539
- if (lines.length > 0) {
2540
- blocks.push({ kind: "lines", variant: "event", lines });
2541
- }
2672
+ const lineBlock = cachedLineBlock(message, maxWidth, buildOptions);
2673
+ if (lineBlock) blocks.push(lineBlock);
2542
2674
  }
2543
2675
  return blocks;
2544
2676
  }
2545
2677
 
2678
+ /**
2679
+ * Non-message blocks (thinking cards, system cards, splash, chrome-less
2680
+ * summaries) go through the terminal line builders, which DO depend on width —
2681
+ * so unlike message blocks these cannot be made width-independent. What they
2682
+ * can avoid is being rebuilt when the width has not moved, which is every poll:
2683
+ * the poll replaces `session`, the portal's useMemo sees a new selectorState,
2684
+ * and the whole transcript is re-derived. Measured at 1500 messages with 10%
2685
+ * line blocks that was 1.17ms a time, and 5.26ms at 50%.
2686
+ *
2687
+ * Only the LAST width is kept per message. A drag sweeps widths and will miss
2688
+ * on every step — that work is genuinely required, since these lines are
2689
+ * wrapped to a column count.
2690
+ *
2691
+ * The compared fields were enumerated from buildChatMessageLines and the
2692
+ * builders it delegates to (message/thinking/system-notice cards). `stopped` is
2693
+ * included because history.js sets it IN PLACE on an existing message.
2694
+ */
2695
+ const chatLineBlockCache = new WeakMap();
2696
+
2697
+ function cachedLineBlock(message, maxWidth, buildOptions) {
2698
+ if (!message || typeof message !== "object") {
2699
+ const lines = buildChatMessageLines(message, maxWidth, buildOptions);
2700
+ return lines.length > 0 ? { kind: "lines", variant: "event", lines } : null;
2701
+ }
2702
+ const cached = chatLineBlockCache.get(message);
2703
+ if (cached
2704
+ && Object.is(cached.maxWidth, maxWidth)
2705
+ && Object.is(cached.text, message.text)
2706
+ && Object.is(cached.mobileText, message.mobileText)
2707
+ && Object.is(cached.role, message.role)
2708
+ && Object.is(cached.time, message.time)
2709
+ && Object.is(cached.createdAt, message.createdAt)
2710
+ && Object.is(cached.splash, message.splash)
2711
+ && Object.is(cached.thinking, message.thinking)
2712
+ && Object.is(cached.thinkingLabel, message.thinkingLabel)
2713
+ && Object.is(cached.progressKind, message.progressKind)
2714
+ && Object.is(cached.noChrome, message.noChrome)
2715
+ && Object.is(cached.cardTitle, message.cardTitle)
2716
+ && Object.is(cached.cardTitleColor, message.cardTitleColor)
2717
+ && Object.is(cached.cardBorderColor, message.cardBorderColor)
2718
+ && Object.is(cached.stopped, message.stopped)
2719
+ && Object.is(cached.tableMode, buildOptions.tableMode)
2720
+ && Object.is(cached.viewerKey, buildOptions.viewerKey)
2721
+ && Object.is(cached.ownerKey, buildOptions.ownerKey)
2722
+ && Object.is(cached.sharedContext, buildOptions.sharedContext)) {
2723
+ return cached.block;
2724
+ }
2725
+ const lines = buildChatMessageLines(message, maxWidth, buildOptions);
2726
+ const block = lines.length > 0 ? { kind: "lines", variant: "event", lines } : null;
2727
+ chatLineBlockCache.set(message, {
2728
+ block,
2729
+ maxWidth,
2730
+ text: message.text,
2731
+ mobileText: message.mobileText,
2732
+ role: message.role,
2733
+ time: message.time,
2734
+ createdAt: message.createdAt,
2735
+ splash: message.splash,
2736
+ thinking: message.thinking,
2737
+ thinkingLabel: message.thinkingLabel,
2738
+ progressKind: message.progressKind,
2739
+ noChrome: message.noChrome,
2740
+ cardTitle: message.cardTitle,
2741
+ cardTitleColor: message.cardTitleColor,
2742
+ cardBorderColor: message.cardBorderColor,
2743
+ stopped: message.stopped,
2744
+ tableMode: buildOptions.tableMode,
2745
+ viewerKey: buildOptions.viewerKey,
2746
+ ownerKey: buildOptions.ownerKey,
2747
+ sharedContext: buildOptions.sharedContext,
2748
+ });
2749
+ return block;
2750
+ }
2751
+
2546
2752
  // A centered inline rule ("──── label ────") for transcript markers. The dash
2547
2753
  // runs are CAPPED (not stretched to maxWidth): the web portal wraps by pixel
2548
2754
  // width, and a maxWidth-long "─" run overflows a narrower pane and wraps the
@@ -12,6 +12,7 @@ import {
12
12
  computeLegacyLayout,
13
13
  createInitialState,
14
14
  createStore,
15
+ AUTO_HISTORY_EVENT_SOFT_CAP,
15
16
  formatCompactNumber,
16
17
  formatCronTimestampForClient,
17
18
  formatHumanDurationSeconds,
@@ -2488,30 +2489,29 @@ function StructuredChatBlocks({ lines, theme, controller = null }) {
2488
2489
  // every other block (splash, thinking cards, system cards, epoch dividers)
2489
2490
  // reuses the structured line renderer so the terminal-era special cases
2490
2491
  // keep exactly one implementation.
2491
- function RichChatBlocks({ blocks, theme, controller = null }) {
2492
- return React.createElement("div", { className: "ps-rich-chat" },
2493
- (blocks || []).map((block, index) => {
2494
- if (block.kind === "message") {
2495
- const header = block.header || {};
2496
- const roleClass = block.role === "user" ? " is-user" : " is-assistant";
2497
- const pendingClass = block.pendingPhase ? ` is-${block.pendingPhase}` : "";
2498
- const text = String(block.text || "");
2499
- // A speaker label only earns its place when it disambiguates:
2500
- // another human in a shared session. The viewer's own turns
2501
- // and the agent's are obvious from side and styling.
2502
- const showLabel = block.role === "user" && header.fromOtherPerson;
2503
- // Consecutive turns from the same speaker read as one
2504
- // continuous passage no repeated header, tighter spacing.
2505
- const previous = blocks[index - 1];
2506
- const continued = previous
2507
- && previous.kind === "message"
2508
- && previous.role === block.role
2509
- && !(previous.header && previous.header.fromOtherPerson)
2510
- && !showLabel;
2511
- return React.createElement("article", {
2512
- key: block.id != null ? `msg:${block.id}` : `msg:${index}`,
2513
- className: `ps-rich-msg${roleClass}${pendingClass}${continued ? " is-continued" : ""}`,
2514
- },
2492
+ /**
2493
+ * One rich message, memoized.
2494
+ *
2495
+ * The body runs markdown through MarkdownPreviewContent — the single most
2496
+ * expensive thing in the transcript — and a pane resize used to re-run it for
2497
+ * every loaded message. `selectChatBlocks` now returns identity-stable blocks,
2498
+ * so this bails out instead. Every prop must stay referentially stable for
2499
+ * that to hold: `theme` comes from a Map, `controller` is a singleton, and
2500
+ * `continued` is hoisted to a boolean because it depends on the PREVIOUS block
2501
+ * and would otherwise have to be recomputed inside.
2502
+ */
2503
+ const RichChatMessage = React.memo(function RichChatMessage({ block, theme, controller, continued }) {
2504
+ const header = block.header || {};
2505
+ const roleClass = block.role === "user" ? " is-user" : " is-assistant";
2506
+ const pendingClass = block.pendingPhase ? ` is-${block.pendingPhase}` : "";
2507
+ const text = String(block.text || "");
2508
+ // A speaker label only earns its place when it disambiguates: another human
2509
+ // in a shared session. The viewer's own turns and the agent's are obvious
2510
+ // from side and styling.
2511
+ const showLabel = block.role === "user" && header.fromOtherPerson;
2512
+ return React.createElement("article", {
2513
+ className: `ps-rich-msg${roleClass}${pendingClass}${continued ? " is-continued" : ""}`,
2514
+ },
2515
2515
  React.createElement("header", { className: "ps-rich-msg-head" },
2516
2516
  showLabel
2517
2517
  ? React.createElement("span", {
@@ -2536,22 +2536,53 @@ function RichChatBlocks({ blocks, theme, controller = null }) {
2536
2536
  ? React.createElement("div", { className: "ps-rich-msg-body" },
2537
2537
  React.createElement(MarkdownPreviewContent, { content: text, theme }))
2538
2538
  : null,
2539
- block.attachments
2540
- ? React.createElement(ArtifactImageStrip, {
2541
- controller,
2542
- sessionId: block.attachments.sessionId,
2543
- attachments: block.attachments.attachments,
2544
- })
2545
- : null);
2539
+ block.attachments
2540
+ ? React.createElement(ArtifactImageStrip, {
2541
+ controller,
2542
+ sessionId: block.attachments.sessionId,
2543
+ attachments: block.attachments.attachments,
2544
+ })
2545
+ : null);
2546
+ });
2547
+
2548
+ /** A non-message block (splash, system card, divider), memoized the same way. */
2549
+ const RichChatLineBlock = React.memo(function RichChatLineBlock({ block, theme, controller }) {
2550
+ const lines = React.useMemo(() => normalizeLines(block.lines || []), [block.lines]);
2551
+ return React.createElement("div", {
2552
+ className: `ps-rich-lineblock is-${block.variant || "event"}`,
2553
+ }, React.createElement(StructuredChatBlocks, { lines, theme, controller }));
2554
+ });
2555
+
2556
+ function RichChatBlocks({ blocks, theme, controller = null }) {
2557
+ return React.createElement("div", { className: "ps-rich-chat" },
2558
+ (blocks || []).map((block, index) => {
2559
+ if (block.kind === "message") {
2560
+ // Consecutive turns from the same speaker read as one
2561
+ // continuous passage — no repeated header, tighter spacing.
2562
+ // Computed here because it reads the previous block; passing it
2563
+ // down as a boolean is what keeps the child memoizable.
2564
+ const previous = blocks[index - 1];
2565
+ const header = block.header || {};
2566
+ const showLabel = block.role === "user" && header.fromOtherPerson;
2567
+ const continued = Boolean(previous
2568
+ && previous.kind === "message"
2569
+ && previous.role === block.role
2570
+ && !(previous.header && previous.header.fromOtherPerson)
2571
+ && !showLabel);
2572
+ return React.createElement(RichChatMessage, {
2573
+ key: block.id != null ? `msg:${block.id}` : `msg:${index}`,
2574
+ block,
2575
+ theme,
2576
+ controller,
2577
+ continued,
2578
+ });
2546
2579
  }
2547
- return React.createElement("div", {
2580
+ return React.createElement(RichChatLineBlock, {
2548
2581
  key: `lines:${index}`,
2549
- className: `ps-rich-lineblock is-${block.variant || "event"}`,
2550
- }, React.createElement(StructuredChatBlocks, {
2551
- lines: normalizeLines(block.lines || []),
2582
+ block,
2552
2583
  theme,
2553
2584
  controller,
2554
- }));
2585
+ });
2555
2586
  }));
2556
2587
  }
2557
2588
 
@@ -3100,6 +3131,10 @@ function SessionDetailBox({ session, childCount = 0 }) {
3100
3131
  : (childCount > 0 ? String(childCount) : null);
3101
3132
 
3102
3133
  return React.createElement("div", { className: "ps-session-detail-box" },
3134
+ // The row above ellipsizes to one line, so this is the only place the
3135
+ // whole name is legible. Two lines are RESERVED whether or not they are
3136
+ // used, so the box height still cannot move with the selection.
3137
+ field("Title", session?.title, "is-title"),
3103
3138
  field("ID", session?.sessionId, "is-id"),
3104
3139
  field("Model", model),
3105
3140
  field("Context", context, percent != null && percent >= 85 ? "is-hot" : percent != null && percent >= 70 ? "is-warm" : ""),
@@ -3273,6 +3308,7 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
3273
3308
  const theme = getTheme(themeId);
3274
3309
  const sessionButtonRefs = React.useRef(new Map());
3275
3310
  const viewState = useControllerSelector(controller, (state) => ({
3311
+ branding: state.branding,
3276
3312
  activeSessionId: state.sessions.activeSessionId,
3277
3313
  sessionsById: state.sessions.byId,
3278
3314
  sessionsFlat: state.sessions.flat,
@@ -3306,7 +3342,12 @@ function SessionPane({ controller, actions = null, panelClassName = "", structur
3306
3342
  connection: {
3307
3343
  mode: viewState.connectionMode,
3308
3344
  },
3309
- }), [viewState.activeSessionId, viewState.auth, viewState.connectionMode, viewState.filterQuery, viewState.ownerFilter, viewState.pinnedIds, viewState.selectedIds, viewState.selectMode, viewState.sessionsById, viewState.sessionsFlat]);
3345
+ // The root system session renders as the deployment's branding title.
3346
+ // Omitting this made the list fall back to "PilotSwarm" on a branded
3347
+ // deployment, while the controller's own calls (which pass real state)
3348
+ // produced the branded name.
3349
+ branding: viewState.branding,
3350
+ }), [viewState.activeSessionId, viewState.auth, viewState.branding, viewState.connectionMode, viewState.filterQuery, viewState.ownerFilter, viewState.pinnedIds, viewState.selectedIds, viewState.selectMode, viewState.sessionsById, viewState.sessionsFlat]);
3310
3351
  // Hold the previous rows when a poll produced identical output.
3311
3352
  const rows = useStableValue(computedRows);
3312
3353
  const activeSession = viewState.activeSessionId
@@ -4044,6 +4085,15 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
4044
4085
  [animatedDots, chrome.animateTitleRight, chrome.titleRight],
4045
4086
  );
4046
4087
  const richMode = viewState.chatViewMode === "rich" && !viewState.activeSessionIsGroup;
4088
+ const [loadingOlder, setLoadingOlder] = React.useState(false);
4089
+ // Scroll-up expands the transcript automatically until the soft cap, then
4090
+ // refuses — and the portal had no control to ask for more, so a busy
4091
+ // session's history became unreachable from the browser. Surface the
4092
+ // control exactly when the automatic path has given up.
4093
+ const showLoadOlder = Boolean(
4094
+ viewState.activeHistory?.hasOlderEvents
4095
+ && Number(viewState.activeHistory?.loadedEventCount || 0) >= AUTO_HISTORY_EVENT_SOFT_CAP,
4096
+ );
4047
4097
  const richBlocks = React.useMemo(
4048
4098
  () => (richMode ? selectChatBlocks(selectorState, viewState.contentWidth, { tableMode: "sentinel" }) : null),
4049
4099
  [richMode, selectorState, viewState.contentWidth],
@@ -4147,6 +4197,20 @@ function ChatPane({ controller, mobile = false, fullWidth = false, showComposer
4147
4197
  className: richMode ? "is-wrapped is-rich" : "is-wrapped",
4148
4198
  panelClassName: richMode ? "ps-chat-panel is-rich" : "ps-chat-panel",
4149
4199
  bottomContent: composer,
4200
+ topContent: showLoadOlder
4201
+ ? React.createElement("div", { className: "ps-load-older-bar" },
4202
+ React.createElement("button", {
4203
+ type: "button",
4204
+ className: "ps-load-older-button",
4205
+ disabled: loadingOlder,
4206
+ onClick: () => {
4207
+ setLoadingOlder(true);
4208
+ controller.handleCommand(UI_COMMANDS.EXPAND_HISTORY)
4209
+ .catch(() => {})
4210
+ .finally(() => setLoadingOlder(false));
4211
+ },
4212
+ }, loadingOlder ? "Loading older messages…" : "↑ Load older messages"))
4213
+ : null,
4150
4214
  structuredBlocks: true,
4151
4215
  renderBody: richBlocks
4152
4216
  ? (_bodyLines, bodyTheme) => React.createElement(RichChatBlocks, {
@@ -1 +1 @@
1
- import{g as p,r as u,d as a}from"./chunk-MOJQB5TN-B-bgmTfa.js";import{p as f}from"./chunk-JWPE2WC7-D5reV4oK.js";import{_ as n,l as o}from"./mermaid.core-OUiCmXKB.js";import{M as c,b as d}from"./cynefin-VYW2F7L2-COwlV5eq.js";import"./pilotswarm-CU6jtK4w.js";var v=d().RailroadAbnf.parser.LangiumParser,i=n(e=>{const r=e.alternatives.map(g);return r.length===1?r[0]:{type:"choice",alternatives:r}},"transformAlternation"),g=n(e=>{const r=e.elements.map(y);return r.length===1?r[0]:{type:"sequence",elements:r}},"transformConcatenation"),b=n(e=>{if(e.includes("*")){const[t,s]=e.split("*"),l=t?parseInt(t,10):0,m=s?parseInt(s,10):1/0;return{min:l,max:m}}const r=parseInt(e,10);return{min:r,max:r}},"parseRepeat"),y=n(e=>{const r=A(e.primary);if(!e.repeat)return r;const{min:t,max:s}=b(e.repeat);return t===0&&s===1?{type:"optional",element:r}:{type:"repetition",element:r,min:t,max:s}},"transformElement"),A=n(e=>{switch(e.$type){case"AbnfStringLiteral":return{type:"terminal",value:e.value};case"AbnfNumVal":return{type:"terminal",value:e.value};case"AbnfRuleName":return{type:"nonterminal",name:e.name};case"AbnfGroup":return i(e.element);case"AbnfOptionalGroup":return{type:"optional",element:i(e.element)};default:throw new Error(`Unsupported ABNF primary node: ${e.$type}`)}},"transformPrimary"),P=n(e=>({name:e.name,definition:i(e.definition)}),"transformRule"),h=n(e=>{f(e,a),e.title&&a.setTitle(e.title),e.rules.map(r=>a.addRule(P(r)))},"populateDb"),R={parse:n(e=>{a.clear(),o.debug("[ABNF Parser] Starting Langium parse");const r=v.parse(e);if(r.lexerErrors.length>0||r.parserErrors.length>0)throw new c(r);const t=r.value;o.debug("[ABNF Parser] Parsed rules:",t.rules.length),h(t),o.debug("[ABNF Parser] Parse complete")},"parse"),parser:{yy:a}},B={parser:R,db:a,renderer:u,styles:p};export{B as diagram};
1
+ import{g as p,r as u,d as a}from"./chunk-MOJQB5TN-D-6WLgUh.js";import{p as f}from"./chunk-JWPE2WC7-BuP_EUE9.js";import{_ as n,l as o}from"./mermaid.core-Dl9SNFGg.js";import{M as c,b as d}from"./cynefin-VYW2F7L2-7QQpYsvS.js";import"./pilotswarm-Bu5uSolS.js";var v=d().RailroadAbnf.parser.LangiumParser,i=n(e=>{const r=e.alternatives.map(g);return r.length===1?r[0]:{type:"choice",alternatives:r}},"transformAlternation"),g=n(e=>{const r=e.elements.map(y);return r.length===1?r[0]:{type:"sequence",elements:r}},"transformConcatenation"),b=n(e=>{if(e.includes("*")){const[t,s]=e.split("*"),l=t?parseInt(t,10):0,m=s?parseInt(s,10):1/0;return{min:l,max:m}}const r=parseInt(e,10);return{min:r,max:r}},"parseRepeat"),y=n(e=>{const r=A(e.primary);if(!e.repeat)return r;const{min:t,max:s}=b(e.repeat);return t===0&&s===1?{type:"optional",element:r}:{type:"repetition",element:r,min:t,max:s}},"transformElement"),A=n(e=>{switch(e.$type){case"AbnfStringLiteral":return{type:"terminal",value:e.value};case"AbnfNumVal":return{type:"terminal",value:e.value};case"AbnfRuleName":return{type:"nonterminal",name:e.name};case"AbnfGroup":return i(e.element);case"AbnfOptionalGroup":return{type:"optional",element:i(e.element)};default:throw new Error(`Unsupported ABNF primary node: ${e.$type}`)}},"transformPrimary"),P=n(e=>({name:e.name,definition:i(e.definition)}),"transformRule"),h=n(e=>{f(e,a),e.title&&a.setTitle(e.title),e.rules.map(r=>a.addRule(P(r)))},"populateDb"),R={parse:n(e=>{a.clear(),o.debug("[ABNF Parser] Starting Langium parse");const r=v.parse(e);if(r.lexerErrors.length>0||r.parserErrors.length>0)throw new c(r);const t=r.value;o.debug("[ABNF Parser] Parsed rules:",t.rules.length),h(t),o.debug("[ABNF Parser] Parse complete")},"parse"),parser:{yy:a}},B={parser:R,db:a,renderer:u,styles:p};export{B as diagram};