agent-relay-server 0.19.3 → 0.21.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.
package/public/index.html CHANGED
@@ -10136,6 +10136,37 @@ var persistImpl = (config, baseOptions) => (set, get, api) => {
10136
10136
  };
10137
10137
  var persist = persistImpl;
10138
10138
  //#endregion
10139
+ //#region ../sdk/src/sse.ts
10140
+ /**
10141
+ * Parse a single SSE frame — the text between blank-line (`\n\n`) separators —
10142
+ * into its event name, data lines, and optional retry hint. Spec-compliant
10143
+ * field parsing: normalises CRLF, skips comment (`:`-prefixed) lines, splits each
10144
+ * line at the first colon, and strips one leading space from the value.
10145
+ */
10146
+ function parseSseFrame(frame) {
10147
+ let event = "message";
10148
+ const data = [];
10149
+ let retry;
10150
+ for (const rawLine of frame.replace(/\r\n/g, "\n").split("\n")) {
10151
+ if (!rawLine || rawLine.startsWith(":")) continue;
10152
+ const sep = rawLine.indexOf(":");
10153
+ const field = sep >= 0 ? rawLine.slice(0, sep) : rawLine;
10154
+ let value = sep >= 0 ? rawLine.slice(sep + 1) : "";
10155
+ if (value.startsWith(" ")) value = value.slice(1);
10156
+ if (field === "event") event = value;
10157
+ else if (field === "data") data.push(value);
10158
+ else if (field === "retry") {
10159
+ const next = Number(value);
10160
+ if (Number.isFinite(next) && next >= 1e3) retry = next;
10161
+ }
10162
+ }
10163
+ return {
10164
+ event,
10165
+ data,
10166
+ retry
10167
+ };
10168
+ }
10169
+ //#endregion
10139
10170
  //#region src/lib/api.ts
10140
10171
  var authToken = "";
10141
10172
  function setAuthToken(token) {
@@ -10190,21 +10221,8 @@ function openRelayEventStream(token, handlers) {
10190
10221
  reconnectTimer = setTimeout(connect, retryMs);
10191
10222
  };
10192
10223
  const dispatchFrame = (frame) => {
10193
- let event = "message";
10194
- const data = [];
10195
- for (const rawLine of frame.replace(/\r\n/g, "\n").split("\n")) {
10196
- if (!rawLine || rawLine.startsWith(":")) continue;
10197
- const separator = rawLine.indexOf(":");
10198
- const field = separator >= 0 ? rawLine.slice(0, separator) : rawLine;
10199
- let value = separator >= 0 ? rawLine.slice(separator + 1) : "";
10200
- if (value.startsWith(" ")) value = value.slice(1);
10201
- if (field === "event") event = value;
10202
- else if (field === "data") data.push(value);
10203
- else if (field === "retry") {
10204
- const nextRetry = Number(value);
10205
- if (Number.isFinite(nextRetry) && nextRetry >= 1e3) retryMs = nextRetry;
10206
- }
10207
- }
10224
+ const { event, data, retry } = parseSseFrame(frame);
10225
+ if (retry) retryMs = retry;
10208
10226
  if (data.length > 0) handlers.message(event, data.join("\n"));
10209
10227
  };
10210
10228
  const connect = async () => {
@@ -10330,25 +10348,27 @@ async function apiBlob(path) {
10330
10348
  return response.blob();
10331
10349
  }
10332
10350
  //#endregion
10333
- //#region src/lib/voice.ts
10351
+ //#region ../sdk/src/speech-text.ts
10334
10352
  /**
10335
- * Browser voice I/O for chat.
10353
+ * Shared text speech preparation, imported by both the dashboard
10354
+ * (`dashboard/src/lib/voice.ts`) and the voice connector
10355
+ * (`connectors/voice/src/text.ts`). Single source of truth — these rules used to
10356
+ * live duplicated in both places and drifted.
10336
10357
  *
10337
- * TTS (output) is 100% browser-native via the Web Speech API — the store
10338
- * already receives every agent response turn, so there is no backend round-trip.
10339
- * STT (input) records the mic and posts the clip to the voice connector's
10340
- * whisper endpoint through the relay proxy (single-origin, auth-gated).
10358
+ * Two stages, applied in order by {@link prepareForSpeech}:
10359
+ * 1. {@link speechify} collapse markdown/code structure into prose worth hearing.
10360
+ * 2. {@link normalizeForSpeech} rule-based normalization of inline tokens
10361
+ * (numbers, units, symbols, code identifiers, paths) so the speech engine
10362
+ * reads them naturally.
10341
10363
  *
10342
- * Speaker policy ("active chat owns the speaker"):
10343
- * - One utterance at a time. Disabled => silent.
10344
- * - Only the active chat speaks, and only responses that arrive while it is
10345
- * active (no backlog replay).
10346
- * - Switching away lets the current utterance finish but drops the now-background
10347
- * chat's queued + future responses.
10348
- * - The active chat preempts: if it speaks while a previous chat's audio lingers,
10349
- * that audio is cancelled.
10364
+ * Why both, and why here: the server-side Kokoro engine (agent-speech) already
10365
+ * normalizes some of this internally (number ranges, decimals, currency) via its
10366
+ * phonemizer but that normalization does NOT run for the browser Web Speech
10367
+ * fallback, and the dashboard's own sentence splitter trips on decimals before
10368
+ * the engine ever sees them. Normalizing here fixes the browser path and protects
10369
+ * the splitter; feeding already-normalized text to Kokoro is harmless (idempotent).
10350
10370
  */
10351
- /** Collapse markdown/code into something worth hearing (mirrors the connector's text.ts). */
10371
+ /** Collapse markdown/code into something worth hearing. */
10352
10372
  function speechify(markdown) {
10353
10373
  if (!markdown) return "";
10354
10374
  let text = markdown.replace(/\r\n/g, "\n");
@@ -10357,6 +10377,7 @@ function speechify(markdown) {
10357
10377
  return ` code block, ${lines} ${lines === 1 ? "line" : "lines"}. `;
10358
10378
  });
10359
10379
  text = text.replace(/```[^\n]*/g, " code block. ");
10380
+ text = collapseTables(text);
10360
10381
  text = text.replace(/^#{1,6}\s+/gm, "");
10361
10382
  text = text.replace(/^\s*[-*+]\s+/gm, ". ");
10362
10383
  text = text.replace(/^\s*\d+\.\s+/gm, ". ");
@@ -10368,6 +10389,127 @@ function speechify(markdown) {
10368
10389
  text = text.replace(/\s*\.\s*\.\s*(\.\s*)+/g, ". ");
10369
10390
  return text.trim();
10370
10391
  }
10392
+ /** A markdown table row: `| a | b |` or `a | b`. The separator row is `|---|:-:|`. */
10393
+ function isTableRow(line) {
10394
+ return /\|/.test(line) && line.trim().length > 0;
10395
+ }
10396
+ function isTableSeparator(line) {
10397
+ return /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/.test(line);
10398
+ }
10399
+ function tableCells(line) {
10400
+ return line.trim().replace(/^\||\|$/g, "").split("|").map((c) => c.trim()).filter((c) => c.length > 0);
10401
+ }
10402
+ /**
10403
+ * Replace contiguous markdown tables with a spoken summary
10404
+ * ("table with columns A, B, C; N rows.") rather than reading pipes and cells.
10405
+ */
10406
+ function collapseTables(text) {
10407
+ const lines = text.split("\n");
10408
+ const out = [];
10409
+ let i = 0;
10410
+ while (i < lines.length) {
10411
+ if (isTableRow(lines[i]) && i + 1 < lines.length && isTableSeparator(lines[i + 1])) {
10412
+ const headers = tableCells(lines[i]);
10413
+ let j = i + 2;
10414
+ let rows = 0;
10415
+ while (j < lines.length && isTableRow(lines[j]) && !isTableSeparator(lines[j])) {
10416
+ rows++;
10417
+ j++;
10418
+ }
10419
+ const cols = headers.length ? `with columns ${headers.join(", ")}` : "";
10420
+ const rowText = `${rows} ${rows === 1 ? "row" : "rows"}`;
10421
+ out.push(`table ${cols ? `${cols}, ` : ""}${rowText}.`.replace(/\s+/g, " ").trim());
10422
+ i = j;
10423
+ continue;
10424
+ }
10425
+ out.push(lines[i]);
10426
+ i++;
10427
+ }
10428
+ return out.join("\n");
10429
+ }
10430
+ var UNIT_WORDS = {
10431
+ ms: "milliseconds",
10432
+ s: "seconds",
10433
+ kb: "kilobytes",
10434
+ mb: "megabytes",
10435
+ gb: "gigabytes",
10436
+ tb: "terabytes"
10437
+ };
10438
+ /** Spell out a fractional part digit-by-digit: "14" → "1 4" (so "3.14" → "3 point 1 4"). */
10439
+ function spaceDigits(frac) {
10440
+ return frac.split("").join(" ");
10441
+ }
10442
+ /**
10443
+ * Ordered normalization rules. ORDER IS LOAD-BEARING:
10444
+ * - URLs/paths first, before anything mangles their slashes/dots.
10445
+ * - `~` → "approximately" before number rules consume the digits after it.
10446
+ * - number ranges ("5-7") before unit expansion, so the unit attaches once: "5 to 7 seconds".
10447
+ * - decimals before unit expansion and before the sentence splitter sees the dot.
10448
+ * - unit expansion last among the number rules.
10449
+ */
10450
+ var SPEECH_RULES = [
10451
+ {
10452
+ name: "url",
10453
+ pattern: /\bhttps?:\/\/\S+/gi,
10454
+ replace: " link "
10455
+ },
10456
+ {
10457
+ name: "path",
10458
+ pattern: /(?:\/[A-Za-z0-9._-]+){2,}\/?/g,
10459
+ replace: (m) => " " + m.replace(/[/._-]+/g, " ").trim() + " "
10460
+ },
10461
+ {
10462
+ name: "func-call",
10463
+ pattern: /\b([A-Za-z_$][\w$]*)\(\)/g,
10464
+ replace: "$1"
10465
+ },
10466
+ {
10467
+ name: "approx",
10468
+ pattern: /~(?=\s*[\d.])/g,
10469
+ replace: "approximately "
10470
+ },
10471
+ {
10472
+ name: "range",
10473
+ pattern: /(\d)\s*[-–—]\s*(?=\d)/g,
10474
+ replace: "$1 to "
10475
+ },
10476
+ {
10477
+ name: "decimal",
10478
+ pattern: /\b(\d+)\.(\d+)\b/g,
10479
+ replace: (_m, i, f) => `${i} point ${spaceDigits(f)}`
10480
+ },
10481
+ {
10482
+ name: "byte-unit",
10483
+ pattern: /\b(\d+)\s?(kb|mb|gb|tb)\b/gi,
10484
+ replace: (_m, n, u) => `${n} ${UNIT_WORDS[u.toLowerCase()]}`
10485
+ },
10486
+ {
10487
+ name: "ms-unit",
10488
+ pattern: /\b(\d{1,5})\s?ms\b/g,
10489
+ replace: (_m, n) => `${n} ${UNIT_WORDS.ms}`
10490
+ },
10491
+ {
10492
+ name: "s-unit",
10493
+ pattern: /\b(\d{1,3})\s?s\b/g,
10494
+ replace: (_m, n) => `${n} ${UNIT_WORDS.s}`
10495
+ }
10496
+ ];
10497
+ /**
10498
+ * Apply the inline normalization rules in order. Pure and idempotent enough to
10499
+ * feed straight into either the Kokoro engine or the browser Web Speech API.
10500
+ */
10501
+ function normalizeForSpeech(text) {
10502
+ if (!text) return "";
10503
+ let out = text;
10504
+ for (const rule of SPEECH_RULES) out = out.replace(rule.pattern, rule.replace);
10505
+ return out.replace(/[ \t]{2,}/g, " ").trim();
10506
+ }
10507
+ /** Full pipeline: markdown → prose → normalized speech text. */
10508
+ function prepareForSpeech(markdown) {
10509
+ return normalizeForSpeech(speechify(markdown));
10510
+ }
10511
+ //#endregion
10512
+ //#region src/lib/voice.ts
10371
10513
  var MAX_CHUNK = 220;
10372
10514
  /** Split into utterance-sized chunks (sentence boundaries; hard-split very long runs). */
10373
10515
  function chunkForSpeech(text) {
@@ -10402,12 +10544,31 @@ var VoiceTts = class {
10402
10544
  gen = 0;
10403
10545
  audioEl = null;
10404
10546
  audioUrl = null;
10547
+ playingKey = null;
10548
+ manual = false;
10549
+ listeners = /* @__PURE__ */ new Set();
10405
10550
  get available() {
10406
10551
  return synthAvailable || typeof Audio !== "undefined";
10407
10552
  }
10408
10553
  isEnabled() {
10409
10554
  return this.enabled;
10410
10555
  }
10556
+ /** Subscribe to manual-playback changes (useSyncExternalStore). */
10557
+ subscribe(fn) {
10558
+ this.listeners.add(fn);
10559
+ return () => {
10560
+ this.listeners.delete(fn);
10561
+ };
10562
+ }
10563
+ /** Id of the bubble currently played on demand, or null. */
10564
+ getPlayingKey() {
10565
+ return this.playingKey;
10566
+ }
10567
+ setPlayingKey(key) {
10568
+ if (key === this.playingKey) return;
10569
+ this.playingKey = key;
10570
+ for (const fn of this.listeners) fn();
10571
+ }
10411
10572
  setEnabled(on) {
10412
10573
  if (on === this.enabled) return;
10413
10574
  this.enabled = on;
@@ -10433,9 +10594,9 @@ var VoiceTts = class {
10433
10594
  /** A captured agent response turn arrived for `chatId`. */
10434
10595
  onResponse(chatId, rawText) {
10435
10596
  if (!this.enabled || !this.available || !chatId || chatId !== this.active) return;
10436
- const text = speechify(rawText);
10597
+ const text = prepareForSpeech(rawText);
10437
10598
  if (!text) return;
10438
- if (this.speaking && this.currentChat && this.currentChat !== chatId) {
10599
+ if (this.speaking && (this.manual || this.currentChat && this.currentChat !== chatId)) {
10439
10600
  this.queue = [];
10440
10601
  this.cancel();
10441
10602
  }
@@ -10445,6 +10606,30 @@ var VoiceTts = class {
10445
10606
  });
10446
10607
  this.pump();
10447
10608
  }
10609
+ /**
10610
+ * Play an arbitrary response on demand (chat-bubble play button), independent
10611
+ * of which chat is active. Preempts whatever is currently playing.
10612
+ */
10613
+ speak(rawText, key) {
10614
+ if (!this.available) return;
10615
+ const text = prepareForSpeech(rawText);
10616
+ if (!text) return;
10617
+ this.reset();
10618
+ this.manual = true;
10619
+ this.speaking = true;
10620
+ this.currentChat = null;
10621
+ this.setPlayingKey(key ?? null);
10622
+ const gen = ++this.gen;
10623
+ const chunks = chunkForSpeech(text);
10624
+ const done = () => {
10625
+ if (gen !== this.gen) return;
10626
+ this.speaking = false;
10627
+ this.manual = false;
10628
+ this.setPlayingKey(null);
10629
+ };
10630
+ if (this.mode === "kokoro") this.speakKokoro(chunks, 0, gen, done);
10631
+ else this.speakBrowser(chunks, 0, gen, done);
10632
+ }
10448
10633
  /** Cut all speech immediately (e.g. when the user starts talking). */
10449
10634
  bargeIn() {
10450
10635
  this.reset();
@@ -10457,6 +10642,8 @@ var VoiceTts = class {
10457
10642
  this.gen++;
10458
10643
  this.currentChat = null;
10459
10644
  this.speaking = false;
10645
+ this.manual = false;
10646
+ this.setPlayingKey(null);
10460
10647
  try {
10461
10648
  window.speechSynthesis.cancel();
10462
10649
  } catch {}
@@ -10479,6 +10666,7 @@ var VoiceTts = class {
10479
10666
  const item = this.queue.shift();
10480
10667
  if (!item) return;
10481
10668
  this.speaking = true;
10669
+ this.manual = false;
10482
10670
  this.currentChat = item.chatId;
10483
10671
  const gen = ++this.gen;
10484
10672
  const chunks = chunkForSpeech(item.text);
@@ -11285,6 +11473,12 @@ function emptyAttention() {
11285
11473
  score: 0
11286
11474
  };
11287
11475
  }
11476
+ function channelIsReady(channel) {
11477
+ if (!channel || channel.status === "offline") return false;
11478
+ if (channel.targetHealth?.status === "ok") return true;
11479
+ if (channel.targetHealth?.status === "error" || channel.targetHealth?.status === "warning") return false;
11480
+ return channel.ready;
11481
+ }
11288
11482
  function channelPresence(channel) {
11289
11483
  if (!channel) return {
11290
11484
  label: "unknown",
@@ -11310,18 +11504,18 @@ function channelPresence(channel) {
11310
11504
  icon: "PlugZap",
11311
11505
  badges: []
11312
11506
  };
11313
- if (!channel.ready) return {
11314
- label: "not ready",
11315
- tone: "warning",
11316
- icon: "Loader",
11317
- badges: []
11318
- };
11319
11507
  if (channel.status === "busy") return {
11320
11508
  label: "busy",
11321
11509
  tone: "warning",
11322
11510
  icon: "Activity",
11323
11511
  badges: []
11324
11512
  };
11513
+ if (!channelIsReady(channel)) return {
11514
+ label: "not ready",
11515
+ tone: "warning",
11516
+ icon: "Loader",
11517
+ badges: []
11518
+ };
11325
11519
  return {
11326
11520
  label: "ready",
11327
11521
  tone: "success",
@@ -11944,6 +12138,7 @@ var useRelayStore = create$1()(persist((set, get) => ({
11944
12138
  agents: [],
11945
12139
  agentsById: {},
11946
12140
  messages: [],
12141
+ threadHistory: {},
11947
12142
  pairs: [],
11948
12143
  tasks: [],
11949
12144
  orchestrators: [],
@@ -12028,6 +12223,7 @@ var useRelayStore = create$1()(persist((set, get) => ({
12028
12223
  chatHasNewItems: false,
12029
12224
  pendingForkImport: null,
12030
12225
  analyticsPeriod: "24h",
12226
+ analyticsData: null,
12031
12227
  inboxReadCursors: {},
12032
12228
  inboxArchivedThreads: {},
12033
12229
  inboxDrafts: {},
@@ -12152,7 +12348,8 @@ var useRelayStore = create$1()(persist((set, get) => ({
12152
12348
  context: true,
12153
12349
  skills: true,
12154
12350
  plugins: true,
12155
- statusLine: true
12351
+ statusLine: true,
12352
+ mcp: true
12156
12353
  },
12157
12354
  skills: [],
12158
12355
  plugins: [],
@@ -12248,6 +12445,7 @@ var useRelayStore = create$1()(persist((set, get) => ({
12248
12445
  s.fetchAgents()
12249
12446
  ]);
12250
12447
  if (view === "files") await s.fetchOrchestrators();
12448
+ if (view === "analytics") await s.fetchAnalytics();
12251
12449
  },
12252
12450
  startClock() {
12253
12451
  useClock.getState().start();
@@ -12362,6 +12560,7 @@ var useRelayStore = create$1()(persist((set, get) => ({
12362
12560
  if (view === "maintenance") work.push(s.fetchMaintenanceJobs());
12363
12561
  if (view === "profiles") work.push(s.fetchAgentProfiles());
12364
12562
  if (view === "workspaces") work.push(s.fetchWorkspaces());
12563
+ if (view === "analytics") work.push(s.fetchAnalytics());
12365
12564
  await Promise.all(work);
12366
12565
  } finally {
12367
12566
  set({ _refreshInFlight: false });
@@ -12372,6 +12571,15 @@ var useRelayStore = create$1()(persist((set, get) => ({
12372
12571
  set({ stats: await api("GET", "/stats") });
12373
12572
  } catch {}
12374
12573
  },
12574
+ async fetchAnalytics() {
12575
+ try {
12576
+ set({ analyticsData: await api("GET", "/stats/analytics?period=" + get().analyticsPeriod) });
12577
+ } catch {}
12578
+ },
12579
+ setAnalyticsPeriod(period) {
12580
+ set({ analyticsPeriod: period });
12581
+ get().fetchAnalytics();
12582
+ },
12375
12583
  async fetchHealth() {
12376
12584
  try {
12377
12585
  set({ health: await api("GET", "/health") });
@@ -12630,6 +12838,16 @@ var useRelayStore = create$1()(persist((set, get) => ({
12630
12838
  set({ messages: mergeFetchedMessages(get().messages, messages) });
12631
12839
  } catch {}
12632
12840
  },
12841
+ async fetchThreadHistory(peer) {
12842
+ if (!peer) return;
12843
+ try {
12844
+ const history = await api("GET", "/messages?for=" + encodeURIComponent(peer) + "&limit=500");
12845
+ set({ threadHistory: {
12846
+ ...get().threadHistory,
12847
+ [peer]: history
12848
+ } });
12849
+ } catch {}
12850
+ },
12633
12851
  async fetchChatHistoryImports() {
12634
12852
  try {
12635
12853
  set({ chatHistoryImports: await api("GET", "/chat/history-imports?limit=500") });
@@ -12708,15 +12926,15 @@ var useRelayStore = create$1()(persist((set, get) => ({
12708
12926
  if (event === "connected") return;
12709
12927
  if (event === "message.new") {
12710
12928
  const msg = JSON.parse(data);
12929
+ if (msg.kind === "session" && msg.from !== "user") {
12930
+ const sess = msg.payload?.session;
12931
+ if (sess?.type === "response" && sess?.origin === "provider") voiceTts.onResponse(inboxPeer(msg), msg.body);
12932
+ }
12711
12933
  const s = get();
12712
12934
  if (s.messages.some((m) => m.id === msg.id)) return;
12713
12935
  const msgs = [...s.messages, msg];
12714
12936
  if (msgs.length > 500) msgs.splice(0, msgs.length - 500);
12715
12937
  set({ messages: msgs });
12716
- if (msg.kind === "session" && msg.from !== "user") {
12717
- const sess = msg.payload?.session;
12718
- if (sess?.type === "response" && sess?.origin === "provider") voiceTts.onResponse(inboxPeer(msg), msg.body);
12719
- }
12720
12938
  const peer = inboxPeer(msg);
12721
12939
  if (isHumanInboundMessage(msg) && peer && s.view === "chat" && s.selectedInboxThread === peer && !isDashboardHidden()) get().markInboxThreadReadTo(peer, msg.id);
12722
12940
  return;
@@ -13240,6 +13458,7 @@ var useRelayStore = create$1()(persist((set, get) => ({
13240
13458
  openInboxThread(peer, messages = []) {
13241
13459
  set({ selectedInboxThread: peer });
13242
13460
  if (messages.length) get().markInboxThreadRead(peer, messages);
13461
+ get().fetchThreadHistory(peer);
13243
13462
  },
13244
13463
  async sendChatMessage(body, thread, attachments = []) {
13245
13464
  if (!body.trim() && attachments.length === 0 || get().chatSending) return;
@@ -13995,7 +14214,8 @@ var useRelayStore = create$1()(persist((set, get) => ({
13995
14214
  context: true,
13996
14215
  skills: true,
13997
14216
  plugins: true,
13998
- statusLine: true
14217
+ statusLine: true,
14218
+ mcp: true
13999
14219
  },
14000
14220
  skills: [],
14001
14221
  plugins: [],
@@ -125295,6 +125515,7 @@ function useChatAgents() {
125295
125515
  ]);
125296
125516
  }
125297
125517
  var NO_STATUS_EVENTS = [];
125518
+ var NO_THREAD_HISTORY = [];
125298
125519
  function useAgentStatusEvents(agentId) {
125299
125520
  return useRelayStore((s) => s.chatStatusEvents[agentId] ?? NO_STATUS_EVENTS);
125300
125521
  }
@@ -126448,9 +126669,14 @@ function AddReaction({ open, onToggle, onReact }) {
126448
126669
  })]
126449
126670
  });
126450
126671
  }
126672
+ function useTtsPlayingKey() {
126673
+ return (0, import_react.useSyncExternalStore)((cb) => voiceTts.subscribe(cb), () => voiceTts.getPlayingKey(), () => null);
126674
+ }
126451
126675
  var MessageBubble = (0, import_react.memo)(function MessageBubble({ msg, peer, onOpenReferencedPath, onPreviewReferencedPath, onPreviewReferencedPathEnd }) {
126452
126676
  const isOutbound = msg.from === HUMAN_AGENT_ID;
126453
126677
  const reactToMessage = useRelayStore((s) => s.reactToMessage);
126678
+ const voiceTtsEnabled = useRelayStore((s) => s.voiceTtsEnabled);
126679
+ const ttsPlayingKey = useTtsPlayingKey();
126454
126680
  const peerCwd = useRelayStore((s) => {
126455
126681
  const cwd = s.agentsById[peer]?.meta?.cwd;
126456
126682
  return typeof cwd === "string" ? cwd : "";
@@ -126465,6 +126691,13 @@ var MessageBubble = (0, import_react.memo)(function MessageBubble({ msg, peer, o
126465
126691
  const reactions = groupedReactions(msg);
126466
126692
  const receipt = isOutbound ? outboundReceipt(msg, peer) : null;
126467
126693
  const ReceiptIcon = receipt?.icon;
126694
+ const isTtsPlaying = ttsPlayingKey === String(msg.id);
126695
+ const showPlayButton = voiceTtsEnabled && !isOutbound && !isReactionEvent(msg) && body.trim().length > 0;
126696
+ function togglePlay(e) {
126697
+ e.stopPropagation();
126698
+ if (isTtsPlaying) voiceTts.bargeIn();
126699
+ else voiceTts.speak(msg.body, String(msg.id));
126700
+ }
126468
126701
  (0, import_react.useEffect)(() => {
126469
126702
  if (!showQuickReact) return;
126470
126703
  function dismiss(e) {
@@ -126516,11 +126749,19 @@ var MessageBubble = (0, import_react.memo)(function MessageBubble({ msg, peer, o
126516
126749
  setShowQuickReact((v) => !v);
126517
126750
  }
126518
126751
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
126752
+ "data-msg-id": msg.id,
126519
126753
  className: cn$2("group/msg flex mb-3", isOutbound ? "justify-end" : "justify-start"),
126520
126754
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
126521
126755
  ref: bubbleRef,
126522
126756
  className: "relative max-w-[85%] md:max-w-[75%]",
126523
126757
  children: [
126758
+ showPlayButton && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
126759
+ type: "button",
126760
+ title: isTtsPlaying ? "Stop playback" : "Play aloud",
126761
+ onClick: togglePlay,
126762
+ className: cn$2("absolute -top-2 -right-2 z-20 inline-flex h-6 w-6 items-center justify-center rounded-full border bg-popover shadow-sm transition", isTtsPlaying ? "border-primary/50 text-primary opacity-100" : "border-border text-muted-foreground opacity-70 hover:bg-muted hover:text-foreground hover:opacity-100 md:opacity-0 md:group-hover/msg:opacity-100"),
126763
+ children: isTtsPlaying ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CircleStop, { className: "h-3.5 w-3.5 animate-pulse" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Volume2, { className: "h-3.5 w-3.5" })
126764
+ }),
126524
126765
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
126525
126766
  className: cn$2("rounded-2xl px-3.5 py-2 text-sm select-text", !isOutbound && !isReactionEvent(msg) && "cursor-pointer", isOutbound ? "bg-primary text-primary-foreground rounded-br-sm" : "bg-card ring-1 ring-foreground/10 rounded-bl-sm"),
126526
126767
  onPointerDown: handleBubblePointerDown,
@@ -126751,6 +126992,109 @@ function sameActivityTurn(a, b) {
126751
126992
  if (at && bt) return at === bt;
126752
126993
  return !at && !bt;
126753
126994
  }
126995
+ function StickyPromptBanner({ scrollRef, timeline }) {
126996
+ const [hidden, setHidden] = (0, import_react.useState)(true);
126997
+ const [expanded, setExpanded] = (0, import_react.useState)(false);
126998
+ const [collapsed, setCollapsed] = (0, import_react.useState)(false);
126999
+ const [promptAbove, setPromptAbove] = (0, import_react.useState)(true);
127000
+ const lastOutbound = (0, import_react.useMemo)(() => {
127001
+ for (let i = timeline.length - 1; i >= 0; i--) {
127002
+ const entry = timeline[i];
127003
+ if (!entry || entry.type !== "message") continue;
127004
+ if (entry.msg.from === "user") return {
127005
+ id: entry.msg.id,
127006
+ body: messageBody(entry.msg)
127007
+ };
127008
+ }
127009
+ return null;
127010
+ }, [timeline]);
127011
+ (0, import_react.useEffect)(() => {
127012
+ const scrollEl = scrollRef.current;
127013
+ if (!lastOutbound || !scrollEl) {
127014
+ setHidden(true);
127015
+ return;
127016
+ }
127017
+ const target = scrollEl.querySelector(`[data-msg-id="${lastOutbound.id}"]`);
127018
+ if (!target) {
127019
+ setHidden(true);
127020
+ return;
127021
+ }
127022
+ const observer = new IntersectionObserver(([e]) => {
127023
+ if (!e) return;
127024
+ setHidden(e.isIntersecting);
127025
+ if (!e.isIntersecting) {
127026
+ const rootTop = e.rootBounds?.top ?? scrollEl.getBoundingClientRect().top;
127027
+ setPromptAbove(e.boundingClientRect.top < rootTop);
127028
+ }
127029
+ }, {
127030
+ root: scrollEl,
127031
+ threshold: 0
127032
+ });
127033
+ observer.observe(target);
127034
+ const onScroll = () => {
127035
+ const r = target.getBoundingClientRect();
127036
+ const sr = scrollEl.getBoundingClientRect();
127037
+ setPromptAbove(r.top < sr.top);
127038
+ };
127039
+ scrollEl.addEventListener("scroll", onScroll, { passive: true });
127040
+ return () => {
127041
+ observer.disconnect();
127042
+ scrollEl.removeEventListener("scroll", onScroll);
127043
+ };
127044
+ }, [lastOutbound?.id, scrollRef]);
127045
+ (0, import_react.useEffect)(() => {
127046
+ setExpanded(false);
127047
+ setCollapsed(false);
127048
+ }, [lastOutbound?.id]);
127049
+ if (hidden || !lastOutbound || !lastOutbound.body.trim()) return null;
127050
+ function scrollToPrompt() {
127051
+ const scrollEl = scrollRef.current;
127052
+ if (!scrollEl || !lastOutbound) return;
127053
+ scrollEl.querySelector(`[data-msg-id="${lastOutbound.id}"]`)?.scrollIntoView({
127054
+ behavior: "smooth",
127055
+ block: "center"
127056
+ });
127057
+ }
127058
+ const arrow = promptAbove ? "↑" : "↓";
127059
+ if (collapsed) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
127060
+ className: "sticky top-0 z-10 -mx-3 md:-mx-4 -mt-3 md:-mt-4 bg-background",
127061
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
127062
+ type: "button",
127063
+ className: "w-full flex items-center justify-center gap-1.5 py-1 text-[11px] text-primary/60 hover:text-primary border-b border-primary/10 transition-colors",
127064
+ onClick: () => setCollapsed(false),
127065
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "Your prompt" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronDown, { className: "w-3 h-3" })]
127066
+ })
127067
+ });
127068
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
127069
+ className: "sticky top-0 z-10 -mx-3 md:-mx-4 -mt-3 md:-mt-4 px-2 md:px-3 pt-2 pb-1.5 bg-background border-b border-primary/10",
127070
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
127071
+ className: "flex items-center gap-2 text-[11px] text-muted-foreground mb-0.5",
127072
+ children: [
127073
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
127074
+ className: "font-medium text-primary/70",
127075
+ children: "Your prompt"
127076
+ }),
127077
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
127078
+ type: "button",
127079
+ className: "ml-auto hover:text-foreground transition-colors",
127080
+ onClick: scrollToPrompt,
127081
+ children: [arrow, " scroll to original"]
127082
+ }),
127083
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
127084
+ type: "button",
127085
+ className: "hover:text-foreground transition-colors",
127086
+ onClick: () => setCollapsed(true),
127087
+ title: "Minimize",
127088
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronUp, { className: "w-3 h-3" })
127089
+ })
127090
+ ]
127091
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
127092
+ className: cn$2("text-sm text-foreground leading-snug cursor-pointer", !expanded && "line-clamp-2"),
127093
+ onClick: () => setExpanded((e) => !e),
127094
+ children: lastOutbound.body
127095
+ })]
127096
+ });
127097
+ }
126754
127098
  function ChatPanel({ threads, onBack, showBackButton }) {
126755
127099
  const selectedInboxThread = useRelayStore((s) => s.selectedInboxThread);
126756
127100
  const agentsById = useRelayStore((s) => s.agentsById);
@@ -126768,6 +127112,7 @@ function ChatPanel({ threads, onBack, showBackButton }) {
126768
127112
  const doAgentAction = useRelayStore((s) => s.doAgentAction);
126769
127113
  const forkFromAgent = useRelayStore((s) => s.forkFromAgent);
126770
127114
  const chatHistoryImports = useRelayStore((s) => s.chatHistoryImports);
127115
+ const peerHistory = useRelayStore((s) => s.threadHistory[s.selectedInboxThread] ?? NO_THREAD_HISTORY);
126771
127116
  const openConfirm = useRelayStore((s) => s.openConfirm);
126772
127117
  const showError = useRelayStore((s) => s.showError);
126773
127118
  const orchestrators = useRelayStore((s) => s.orchestrators);
@@ -126805,6 +127150,18 @@ function ChatPanel({ threads, onBack, showBackButton }) {
126805
127150
  const filePreviewRequest = (0, import_react.useRef)(0);
126806
127151
  const statusEvents = useAgentStatusEvents(selectedInboxThread);
126807
127152
  const thread = (0, import_react.useMemo)(() => threads.find((t) => t.peer === selectedInboxThread) || null, [threads, selectedInboxThread]);
127153
+ const threadMessages = (0, import_react.useMemo)(() => {
127154
+ const live = thread?.messages ?? [];
127155
+ if (peerHistory.length === 0) return live;
127156
+ const byId = /* @__PURE__ */ new Map();
127157
+ for (const m of peerHistory) if (inboxPeer(m) === selectedInboxThread && !isReactionEventMessage(m)) byId.set(m.id, m);
127158
+ for (const m of live) byId.set(m.id, m);
127159
+ return [...byId.values()].sort((a, b) => a.id - b.id);
127160
+ }, [
127161
+ thread?.messages,
127162
+ peerHistory,
127163
+ selectedInboxThread
127164
+ ]);
126808
127165
  const agent = agentsById[selectedInboxThread] || null;
126809
127166
  const agentSpawnRequestId = typeof agent?.meta?.spawnRequestId === "string" ? agent.meta.spawnRequestId : "";
126810
127167
  const importedHistory = (0, import_react.useMemo)(() => {
@@ -126824,7 +127181,7 @@ function ChatPanel({ threads, onBack, showBackButton }) {
126824
127181
  const draft = inboxDrafts[selectedInboxThread] || "";
126825
127182
  const importedEntryCount = importedHistory.reduce((sum, history) => sum + history.entries.length, 0);
126826
127183
  const pinnedScroll = usePinnedScroll(selectedInboxThread, [
126827
- thread?.messages.length,
127184
+ threadMessages.length,
126828
127185
  statusEvents.length,
126829
127186
  importedEntryCount,
126830
127187
  pendingApproval?.id,
@@ -126880,8 +127237,8 @@ function ChatPanel({ threads, onBack, showBackButton }) {
126880
127237
  if (filePreviewCloseTimer.current !== null) window.clearTimeout(filePreviewCloseTimer.current);
126881
127238
  };
126882
127239
  }, []);
126883
- const timeline = (0, import_react.useMemo)(() => buildTimeline(thread?.messages || [], statusEvents, agent?.createdAt, importedHistory), [
126884
- thread?.messages,
127240
+ const timeline = (0, import_react.useMemo)(() => buildTimeline(threadMessages, statusEvents, agent?.createdAt, importedHistory), [
127241
+ threadMessages,
126885
127242
  statusEvents,
126886
127243
  agent?.createdAt,
126887
127244
  importedHistory
@@ -127398,6 +127755,10 @@ function ChatPanel({ threads, onBack, showBackButton }) {
127398
127755
  children: "No messages yet"
127399
127756
  })]
127400
127757
  }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
127758
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StickyPromptBanner, {
127759
+ scrollRef: pinnedScroll.ref,
127760
+ timeline
127761
+ }),
127401
127762
  timeline.map((entry) => {
127402
127763
  if (entry.type === "message") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MessageBubble, {
127403
127764
  msg: entry.msg,
@@ -129832,7 +130193,7 @@ function ChannelCard({ channel }) {
129832
130193
  }
129833
130194
  function ChannelsView() {
129834
130195
  const channels = useRelayStore((s) => s.channels);
129835
- const readyCount = channels.filter((c) => c.status !== "offline" && c.ready).length;
130196
+ const readyCount = channels.filter(channelIsReady).length;
129836
130197
  const errorCount = channels.filter((c) => c.targetHealth?.status === "error").length;
129837
130198
  const warningCount = channels.filter((c) => c.targetHealth?.status === "warning").length;
129838
130199
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
@@ -152495,13 +152856,14 @@ var PERIODS = [
152495
152856
  function periodMs(id) {
152496
152857
  return PERIODS.find((p) => p.id === id)?.ms ?? 864e5;
152497
152858
  }
152498
- function categorizeMessage(msg) {
152499
- if (msg.claimable) return "Work items";
152500
- if (msg.kind === "system" || msg.from === "system") return "System";
152501
- if (msg.kind === "pair") return "Pair";
152502
- if (msg.kind === "channel.event") return "Channel";
152503
- return "Messages";
152504
- }
152859
+ var CATEGORY_ORDER = [
152860
+ "Messages",
152861
+ "Replies",
152862
+ "Work items",
152863
+ "System",
152864
+ "Pair",
152865
+ "Channel"
152866
+ ];
152505
152867
  function StatCard({ label, value, icon: Icon, className = "" }) {
152506
152868
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Card, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardContent, {
152507
152869
  className: "pt-4",
@@ -152522,11 +152884,11 @@ function StatCard({ label, value, icon: Icon, className = "" }) {
152522
152884
  }
152523
152885
  function PeriodSelector() {
152524
152886
  const period = useRelayStore((s) => s.analyticsPeriod);
152525
- const set = useRelayStore((s) => s.set);
152887
+ const setPeriod = useRelayStore((s) => s.setAnalyticsPeriod);
152526
152888
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
152527
152889
  className: "flex items-center gap-1 bg-muted rounded-lg p-1",
152528
152890
  children: PERIODS.map((p) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
152529
- onClick: () => set({ analyticsPeriod: p.id }),
152891
+ onClick: () => setPeriod(p.id),
152530
152892
  className: `px-3 py-1 rounded-md text-xs font-medium transition-colors ${period === p.id ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`,
152531
152893
  children: p.label
152532
152894
  }, p.id))
@@ -152566,67 +152928,38 @@ function useChart(id, getOptions, deps) {
152566
152928
  }
152567
152929
  function AnalyticsView() {
152568
152930
  const agents = useRelayStore((s) => s.agents);
152569
- const messages = useRelayStore((s) => s.messages);
152570
- useRelayStore((s) => s.stats);
152931
+ const data = useRelayStore((s) => s.analyticsData);
152571
152932
  const period = useRelayStore((s) => s.analyticsPeriod);
152572
152933
  const theme = useRelayStore((s) => s.theme);
152573
152934
  const onlineCount = useOnlineCount();
152574
152935
  const busyCount = useBusyCount();
152575
152936
  const isDark = THEMES.find((t) => t.id === theme)?.dark ?? true;
152576
- const cutoff = Date.now() - periodMs(period);
152577
- const filteredMessages = (0, import_react.useMemo)(() => {
152578
- return messages.filter((m) => {
152579
- const ts = toTimestamp(m.createdAt);
152580
- return ts && ts >= cutoff;
152581
- });
152582
- }, [messages, cutoff]);
152583
152937
  const volumeData = (0, import_react.useMemo)(() => {
152584
- const ms = periodMs(period);
152585
- const bucketCount = period === "1h" ? 12 : period === "6h" ? 12 : period === "24h" ? 24 : period === "7d" ? 14 : period === "14d" ? 14 : 15;
152586
- const bucketMs = ms / bucketCount;
152587
- const now = Date.now();
152588
- const categories = [
152589
- "Messages",
152590
- "Replies",
152591
- "Work items",
152592
- "System",
152593
- "Pair",
152594
- "Channel"
152595
- ];
152596
- const series = {};
152597
- for (const cat of categories) series[cat] = new Array(bucketCount).fill(0);
152598
- const labels = [];
152599
- for (let i = 0; i < bucketCount; i++) {
152600
- const t = /* @__PURE__ */ new Date(now - (bucketCount - 1 - i) * bucketMs);
152601
- if (ms <= 864e5) labels.push(t.toLocaleTimeString(void 0, {
152602
- hour: "2-digit",
152603
- minute: "2-digit"
152604
- }));
152605
- else labels.push(t.toLocaleDateString(void 0, {
152606
- month: "short",
152607
- day: "numeric"
152608
- }));
152609
- }
152610
- for (const msg of filteredMessages) {
152611
- const ts = toTimestamp(msg.createdAt);
152612
- if (!ts) continue;
152613
- const bucketIdx = Math.floor((ts - (now - ms)) / bucketMs);
152614
- if (bucketIdx < 0 || bucketIdx >= bucketCount) continue;
152615
- const bucket = series[categorizeMessage(msg)];
152616
- if (bucket) bucket[bucketIdx] = (bucket[bucketIdx] ?? 0) + 1;
152617
- }
152938
+ if (!data) return {
152939
+ labels: [],
152940
+ series: []
152941
+ };
152942
+ const asTime = periodMs(period) <= 864e5;
152618
152943
  return {
152619
- labels,
152620
- series: categories.flatMap((c) => {
152621
- const data = series[c];
152622
- if (!data || !data.some((v) => v > 0)) return [];
152623
- return [{
152624
- name: c,
152625
- data
152626
- }];
152944
+ labels: data.buckets.map((b) => {
152945
+ const t = new Date(b.start);
152946
+ return asTime ? t.toLocaleTimeString(void 0, {
152947
+ hour: "2-digit",
152948
+ minute: "2-digit"
152949
+ }) : t.toLocaleDateString(void 0, {
152950
+ month: "short",
152951
+ day: "numeric"
152952
+ });
152953
+ }),
152954
+ series: CATEGORY_ORDER.flatMap((cat) => {
152955
+ const series = data.buckets.map((b) => b.counts[cat] ?? 0);
152956
+ return series.some((v) => v > 0) ? [{
152957
+ name: cat,
152958
+ data: series
152959
+ }] : [];
152627
152960
  })
152628
152961
  };
152629
- }, [filteredMessages, period]);
152962
+ }, [data, period]);
152630
152963
  const utilizationData = (0, import_react.useMemo)(() => {
152631
152964
  let idle = 0, busy = 0;
152632
152965
  for (const a of agents) {
@@ -152641,14 +152974,7 @@ function AnalyticsView() {
152641
152974
  };
152642
152975
  }, [agents]);
152643
152976
  const heatmapData = (0, import_react.useMemo)(() => {
152644
- const grid = Array.from({ length: 7 }, () => new Array(24).fill(0));
152645
- for (const msg of filteredMessages) {
152646
- const ts = toTimestamp(msg.createdAt);
152647
- if (!ts) continue;
152648
- const d = new Date(ts);
152649
- const row = grid[d.getDay()];
152650
- if (row) row[d.getHours()] = (row[d.getHours()] ?? 0) + 1;
152651
- }
152977
+ const grid = data?.heatmap ?? Array.from({ length: 7 }, () => new Array(24).fill(0));
152652
152978
  return [
152653
152979
  "Sun",
152654
152980
  "Mon",
@@ -152659,24 +152985,20 @@ function AnalyticsView() {
152659
152985
  "Sat"
152660
152986
  ].map((name, dayIdx) => ({
152661
152987
  name,
152662
- data: (grid[dayIdx] ?? []).map((v, hour) => ({
152988
+ data: (grid[dayIdx] ?? new Array(24).fill(0)).map((v, hour) => ({
152663
152989
  x: String(hour).padStart(2, "0"),
152664
152990
  y: v
152665
152991
  }))
152666
152992
  }));
152667
- }, [filteredMessages]);
152993
+ }, [data]);
152668
152994
  const activityBreakdown = (0, import_react.useMemo)(() => {
152669
- const counts = {};
152670
- for (const msg of filteredMessages) {
152671
- const cat = categorizeMessage(msg);
152672
- counts[cat] = (counts[cat] ?? 0) + 1;
152673
- }
152674
- const entries = Object.entries(counts).filter(([, v]) => v > 0).sort((a, b) => b[1] - a[1]);
152995
+ const counts = data?.categories;
152996
+ const entries = counts ? Object.entries(counts).filter(([, v]) => v > 0).sort((a, b) => b[1] - a[1]) : [];
152675
152997
  return {
152676
152998
  labels: entries.map(([k]) => k),
152677
152999
  values: entries.map(([, v]) => v)
152678
153000
  };
152679
- }, [filteredMessages]);
153001
+ }, [data]);
152680
153002
  const chartColors = isDark ? {
152681
153003
  text: "rgba(255,255,255,0.4)",
152682
153004
  grid: "rgba(255,255,255,0.06)",
@@ -152815,8 +153137,9 @@ function AnalyticsView() {
152815
153137
  }] }
152816
153138
  } }
152817
153139
  }), [heatmapData, isDark]);
152818
- const periodMessageCount = filteredMessages.length;
152819
- const reactionCount = filteredMessages.reduce((sum, m) => sum + (m.reactions?.length ?? 0), 0);
153140
+ const periodMessageCount = data?.totalMessages ?? 0;
153141
+ const reactionCount = data?.totalReactions ?? 0;
153142
+ const hasHeatmapData = (data?.totalMessages ?? 0) > 0;
152820
153143
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
152821
153144
  className: "space-y-6",
152822
153145
  children: [
@@ -152882,7 +153205,7 @@ function AnalyticsView() {
152882
153205
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardTitle, {
152883
153206
  className: "text-sm",
152884
153207
  children: "Busiest hours"
152885
- }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardContent, { children: filteredMessages.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153208
+ }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardContent, { children: !hasHeatmapData ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
152886
153209
  className: "flex items-center justify-center h-[240px] text-muted-foreground text-sm",
152887
153210
  children: "No data for heatmap"
152888
153211
  }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { id: "analytics-heatmap-chart" }) })] })
@@ -157587,6 +157910,29 @@ function AgentProfileModal() {
157587
157910
  ...partial
157588
157911
  } });
157589
157912
  }
157913
+ function updateCodexToolOutputTokenLimit(value) {
157914
+ const providerOptions = profile.providerOptions || {};
157915
+ const codex = { ...providerOptions.codex && typeof providerOptions.codex === "object" && !Array.isArray(providerOptions.codex) ? providerOptions.codex : {} };
157916
+ if (!value.trim()) delete codex.toolOutputTokenLimit;
157917
+ else {
157918
+ const limit = Number(value);
157919
+ if (!Number.isSafeInteger(limit)) return;
157920
+ codex.toolOutputTokenLimit = limit;
157921
+ }
157922
+ updateProfileModal({ providerOptions: {
157923
+ ...providerOptions,
157924
+ codex
157925
+ } });
157926
+ }
157927
+ function updateMaxSpawnedAgents(value) {
157928
+ if (!value.trim()) {
157929
+ updateProfileModal({ maxSpawnedAgents: void 0 });
157930
+ return;
157931
+ }
157932
+ const n = Number(value);
157933
+ if (!Number.isInteger(n) || n < 0) return;
157934
+ updateProfileModal({ maxSpawnedAgents: Math.min(n, 100) });
157935
+ }
157590
157936
  function selectBase(base) {
157591
157937
  const isHost = base === "host";
157592
157938
  updateProfileModal({
@@ -157595,7 +157941,8 @@ function AgentProfileModal() {
157595
157941
  context: isHost,
157596
157942
  skills: isHost,
157597
157943
  plugins: isHost,
157598
- statusLine: isHost
157944
+ statusLine: isHost,
157945
+ mcp: isHost
157599
157946
  },
157600
157947
  mcp: {
157601
157948
  ...profile.mcp,
@@ -157612,6 +157959,8 @@ function AgentProfileModal() {
157612
157959
  }
157613
157960
  });
157614
157961
  }
157962
+ const codexOptions = profile.providerOptions?.codex && typeof profile.providerOptions.codex === "object" && !Array.isArray(profile.providerOptions.codex) ? profile.providerOptions.codex : {};
157963
+ const codexToolOutputTokenLimit = Number.isSafeInteger(codexOptions.toolOutputTokenLimit) ? String(codexOptions.toolOutputTokenLimit) : "";
157615
157964
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Dialog, {
157616
157965
  open,
157617
157966
  onOpenChange: (o) => !o && closeProfileModal(),
@@ -157695,14 +158044,15 @@ function AgentProfileModal() {
157695
158044
  "context",
157696
158045
  "skills",
157697
158046
  "plugins",
157698
- "statusLine"
158047
+ "statusLine",
158048
+ "mcp"
157699
158049
  ].map((key) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
157700
158050
  className: "flex items-center justify-between gap-2",
157701
158051
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, {
157702
158052
  className: "capitalize",
157703
- children: key === "statusLine" ? "Status Line" : key
158053
+ children: key === "statusLine" ? "Status Line" : key === "mcp" ? "MCP Tools" : key
157704
158054
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Switch, {
157705
- checked: profile.relay[key],
158055
+ checked: profile.relay[key] ?? true,
157706
158056
  onCheckedChange: (v) => updateRelay({ [key]: v }),
157707
158057
  disabled: readOnly
157708
158058
  })]
@@ -157748,6 +158098,27 @@ function AgentProfileModal() {
157748
158098
  })] })]
157749
158099
  })
157750
158100
  }),
158101
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Section, {
158102
+ title: "Spawning",
158103
+ defaultOpen: false,
158104
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
158105
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, { children: "Spawn Quota" }),
158106
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
158107
+ type: "number",
158108
+ min: 0,
158109
+ max: 100,
158110
+ step: 1,
158111
+ value: profile.maxSpawnedAgents === void 0 ? "" : String(profile.maxSpawnedAgents),
158112
+ onChange: (e) => updateMaxSpawnedAgents(e.target.value),
158113
+ placeholder: "0",
158114
+ disabled: readOnly
158115
+ }),
158116
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
158117
+ className: "mt-1 text-xs text-muted-foreground",
158118
+ children: "Max concurrent live child agents this profile may spawn. Empty or 0 = cannot spawn (default). Children never inherit it (no grandchildren)."
158119
+ })
158120
+ ] })
158121
+ }),
157751
158122
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Section, {
157752
158123
  title: "Environment Variables",
157753
158124
  defaultOpen: false,
@@ -157757,10 +158128,18 @@ function AgentProfileModal() {
157757
158128
  disabled: readOnly
157758
158129
  })
157759
158130
  }),
157760
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Section, {
158131
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Section, {
157761
158132
  title: "Provider Options",
157762
158133
  defaultOpen: false,
157763
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Textarea, {
158134
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, { children: "Codex Tool Output Limit" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
158135
+ type: "number",
158136
+ min: 1e3,
158137
+ max: 2e5,
158138
+ value: codexToolOutputTokenLimit,
158139
+ onChange: (e) => updateCodexToolOutputTokenLimit(e.target.value),
158140
+ placeholder: "12000",
158141
+ disabled: readOnly
158142
+ })] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Textarea, {
157764
158143
  value: JSON.stringify(profile.providerOptions || {}, null, 2),
157765
158144
  onChange: (e) => {
157766
158145
  try {
@@ -157771,7 +158150,7 @@ function AgentProfileModal() {
157771
158150
  disabled: readOnly,
157772
158151
  className: "font-mono text-xs",
157773
158152
  placeholder: "{}"
157774
- })
158153
+ })]
157775
158154
  })
157776
158155
  ]
157777
158156
  }),
@@ -158603,6 +158982,10 @@ if ("serviceWorker" in navigator) {
158603
158982
  inset-block: calc(var(--spacing) * 0);
158604
158983
  }
158605
158984
 
158985
+ .-top-2 {
158986
+ top: calc(var(--spacing) * -2);
158987
+ }
158988
+
158606
158989
  .top-0 {
158607
158990
  top: calc(var(--spacing) * 0);
158608
158991
  }
@@ -158631,6 +159014,10 @@ if ("serviceWorker" in navigator) {
158631
159014
  right: calc(var(--spacing) * -.5);
158632
159015
  }
158633
159016
 
159017
+ .-right-2 {
159018
+ right: calc(var(--spacing) * -2);
159019
+ }
159020
+
158634
159021
  .right-2 {
158635
159022
  right: calc(var(--spacing) * 2);
158636
159023
  }
@@ -158765,6 +159152,10 @@ if ("serviceWorker" in navigator) {
158765
159152
  margin-inline: calc(var(--spacing) * -1);
158766
159153
  }
158767
159154
 
159155
+ .-mx-3 {
159156
+ margin-inline: calc(var(--spacing) * -3);
159157
+ }
159158
+
158768
159159
  .-mx-4 {
158769
159160
  margin-inline: calc(var(--spacing) * -4);
158770
159161
  }
@@ -158789,6 +159180,10 @@ if ("serviceWorker" in navigator) {
158789
159180
  margin-top: calc(var(--spacing) * -1);
158790
159181
  }
158791
159182
 
159183
+ .-mt-3 {
159184
+ margin-top: calc(var(--spacing) * -3);
159185
+ }
159186
+
158792
159187
  .mt-0\.5 {
158793
159188
  margin-top: calc(var(--spacing) * .5);
158794
159189
  }
@@ -160179,6 +160574,16 @@ if ("serviceWorker" in navigator) {
160179
160574
  }
160180
160575
  }
160181
160576
 
160577
+ .border-primary\/10 {
160578
+ border-color: var(--primary);
160579
+ }
160580
+
160581
+ @supports (color: color-mix(in lab, red, red)) {
160582
+ .border-primary\/10 {
160583
+ border-color: color-mix(in oklab, var(--primary) 10%, transparent);
160584
+ }
160585
+ }
160586
+
160182
160587
  .border-primary\/25 {
160183
160588
  border-color: var(--primary);
160184
160589
  }
@@ -161219,6 +161624,10 @@ if ("serviceWorker" in navigator) {
161219
161624
  padding-bottom: calc(var(--spacing) * 1);
161220
161625
  }
161221
161626
 
161627
+ .pb-1\.5 {
161628
+ padding-bottom: calc(var(--spacing) * 1.5);
161629
+ }
161630
+
161222
161631
  .pb-2 {
161223
161632
  padding-bottom: calc(var(--spacing) * 2);
161224
161633
  }
@@ -162436,6 +162845,10 @@ if ("serviceWorker" in navigator) {
162436
162845
  .hover\:opacity-80:hover {
162437
162846
  opacity: .8;
162438
162847
  }
162848
+
162849
+ .hover\:opacity-100:hover {
162850
+ opacity: 1;
162851
+ }
162439
162852
  }
162440
162853
 
162441
162854
  .focus\:bg-accent:focus {
@@ -163003,6 +163416,14 @@ if ("serviceWorker" in navigator) {
163003
163416
  margin: calc(var(--spacing) * -6);
163004
163417
  }
163005
163418
 
163419
+ .md\:-mx-4 {
163420
+ margin-inline: calc(var(--spacing) * -4);
163421
+ }
163422
+
163423
+ .md\:-mt-4 {
163424
+ margin-top: calc(var(--spacing) * -4);
163425
+ }
163426
+
163006
163427
  .md\:block {
163007
163428
  display: block;
163008
163429
  }
@@ -163090,6 +163511,10 @@ if ("serviceWorker" in navigator) {
163090
163511
  padding: calc(var(--spacing) * 6);
163091
163512
  }
163092
163513
 
163514
+ .md\:px-3 {
163515
+ padding-inline: calc(var(--spacing) * 3);
163516
+ }
163517
+
163093
163518
  .md\:px-4 {
163094
163519
  padding-inline: calc(var(--spacing) * 4);
163095
163520
  }
@@ -163111,6 +163536,10 @@ if ("serviceWorker" in navigator) {
163111
163536
  text-wrap: pretty;
163112
163537
  }
163113
163538
 
163539
+ .md\:opacity-0 {
163540
+ opacity: 0;
163541
+ }
163542
+
163114
163543
  @media (hover: hover) {
163115
163544
  .md\:group-hover\/msg\:pointer-events-auto:is(:where(.group\/msg):hover *) {
163116
163545
  pointer-events: auto;