agent-relay-server 0.20.0 → 0.22.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",
@@ -11905,9 +12099,11 @@ function isDashboardHidden() {
11905
12099
  function notificationPeer(notification) {
11906
12100
  return notification.threadPeer || notification.agentId || "";
11907
12101
  }
12102
+ function isActiveVisibleChat(peer, state) {
12103
+ return Boolean(peer && state.view === "chat" && state.selectedInboxThread === peer && !isDashboardHidden());
12104
+ }
11908
12105
  function notificationTargetsActiveChat(notification, state) {
11909
- const peer = notificationPeer(notification);
11910
- return Boolean(state.view === "chat" && peer && state.selectedInboxThread === peer && !isDashboardHidden());
12106
+ return isActiveVisibleChat(notificationPeer(notification), state);
11911
12107
  }
11912
12108
  function lastInboundMessageId(messages) {
11913
12109
  return messages.filter((m) => m.to === "user" && m.from !== "user").reduce((max, m) => Math.max(max, m.id), 0);
@@ -12641,7 +12837,15 @@ var useRelayStore = create$1()(persist((set, get) => ({
12641
12837
  if (s.view === "messages" && s.selectedAgent) path += "&for=" + encodeURIComponent(s.selectedAgent);
12642
12838
  if (s.view === "messages" && s.channelFilter) path += "&channel=" + encodeURIComponent(s.channelFilter);
12643
12839
  const messages = await api("GET", path);
12644
- set({ messages: mergeFetchedMessages(get().messages, messages) });
12840
+ const merged = mergeFetchedMessages(get().messages, messages);
12841
+ set({ messages: merged });
12842
+ const after = get();
12843
+ const peer = after.selectedInboxThread;
12844
+ if (isActiveVisibleChat(peer, after)) {
12845
+ let lastId = 0;
12846
+ for (const m of merged) if (m.id > lastId && inboxPeer(m) === peer && isHumanInboundMessage(m) && !isSessionActivityStep(m)) lastId = m.id;
12847
+ if (lastId) get().markInboxThreadReadTo(peer, lastId);
12848
+ }
12645
12849
  } catch {}
12646
12850
  },
12647
12851
  async fetchThreadHistory(peer) {
@@ -12732,17 +12936,17 @@ var useRelayStore = create$1()(persist((set, get) => ({
12732
12936
  if (event === "connected") return;
12733
12937
  if (event === "message.new") {
12734
12938
  const msg = JSON.parse(data);
12939
+ if (msg.kind === "session" && msg.from !== "user") {
12940
+ const sess = msg.payload?.session;
12941
+ if (sess?.type === "response" && sess?.origin === "provider") voiceTts.onResponse(inboxPeer(msg), msg.body);
12942
+ }
12735
12943
  const s = get();
12736
12944
  if (s.messages.some((m) => m.id === msg.id)) return;
12737
12945
  const msgs = [...s.messages, msg];
12738
12946
  if (msgs.length > 500) msgs.splice(0, msgs.length - 500);
12739
12947
  set({ messages: msgs });
12740
- if (msg.kind === "session" && msg.from !== "user") {
12741
- const sess = msg.payload?.session;
12742
- if (sess?.type === "response" && sess?.origin === "provider") voiceTts.onResponse(inboxPeer(msg), msg.body);
12743
- }
12744
12948
  const peer = inboxPeer(msg);
12745
- if (isHumanInboundMessage(msg) && peer && s.view === "chat" && s.selectedInboxThread === peer && !isDashboardHidden()) get().markInboxThreadReadTo(peer, msg.id);
12949
+ if (isHumanInboundMessage(msg) && isActiveVisibleChat(peer, s)) get().markInboxThreadReadTo(peer, msg.id);
12746
12950
  return;
12747
12951
  }
12748
12952
  if (event === "message.queued" || event === "message.expired" || event === "message.delivery_updated" || event === "message.reaction_updated") {
@@ -99607,6 +99811,45 @@ function Button({ className, variant = "default", size = "default", asChild = fa
99607
99811
  });
99608
99812
  }
99609
99813
  //#endregion
99814
+ //#region src/components/shared/copy-button.tsx
99815
+ /**
99816
+ * Shared copy-to-clipboard button with a transient "copied" check state.
99817
+ * Consolidates the duplicated clipboard + timeout pattern used across views.
99818
+ */
99819
+ function CopyButton({ value, label = "Copy", copiedLabel = "Copied", showText = false, size, variant = "ghost", className, iconClassName, disabled, onCopied }) {
99820
+ const [copied, setCopied] = (0, import_react.useState)(false);
99821
+ const timer = (0, import_react.useRef)(null);
99822
+ (0, import_react.useEffect)(() => () => {
99823
+ if (timer.current) clearTimeout(timer.current);
99824
+ }, []);
99825
+ async function copy(e) {
99826
+ e.preventDefault();
99827
+ e.stopPropagation();
99828
+ try {
99829
+ await navigator.clipboard?.writeText(typeof value === "function" ? value() : value);
99830
+ setCopied(true);
99831
+ onCopied?.();
99832
+ if (timer.current) clearTimeout(timer.current);
99833
+ timer.current = setTimeout(() => setCopied(false), 1400);
99834
+ } catch {
99835
+ setCopied(false);
99836
+ }
99837
+ }
99838
+ const resolvedSize = size ?? (showText ? "sm" : "icon-sm");
99839
+ const iconCls = cn$2("h-3.5 w-3.5", iconClassName);
99840
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
99841
+ type: "button",
99842
+ size: resolvedSize,
99843
+ variant,
99844
+ className,
99845
+ disabled,
99846
+ title: copied ? copiedLabel : label,
99847
+ "aria-label": copied ? copiedLabel : label,
99848
+ onClick: (e) => void copy(e),
99849
+ children: [copied ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Check, { className: iconCls }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Copy, { className: iconCls }), showText && (copied ? copiedLabel : label)]
99850
+ });
99851
+ }
99852
+ //#endregion
99610
99853
  //#region \0vite/preload-helper.js
99611
99854
  var scriptRel, assetsURL, seen, __vitePreload;
99612
99855
  var init_preload_helper = __esmMin((() => {
@@ -108271,25 +108514,14 @@ var CodePreview = (0, import_react.memo)(function CodePreview({ content, path, m
108271
108514
  const [html, setHtml] = (0, import_react.useState)("");
108272
108515
  const [loading, setLoading] = (0, import_react.useState)(false);
108273
108516
  const [failed, setFailed] = (0, import_react.useState)(false);
108274
- const [copied, setCopied] = (0, import_react.useState)(false);
108275
- async function copyCode() {
108276
- try {
108277
- await navigator.clipboard?.writeText(content);
108278
- setCopied(true);
108279
- window.setTimeout(() => setCopied(false), 1400);
108280
- } catch {
108281
- setCopied(false);
108282
- }
108283
- }
108284
108517
  function copyButton() {
108285
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
108286
- type: "button",
108518
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CopyButton, {
108519
+ value: content,
108520
+ label: "Copy code",
108521
+ copiedLabel: "Copied code",
108287
108522
  size: "icon",
108288
108523
  variant: "ghost",
108289
- className: "absolute right-2 top-2 h-7 w-7 bg-background/80 opacity-0 shadow-sm backdrop-blur transition-opacity hover:bg-muted group-hover/code:opacity-100 focus-visible:opacity-100",
108290
- onClick: copyCode,
108291
- title: copied ? "Copied code" : "Copy code",
108292
- children: copied ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Check, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Copy, { className: "h-3.5 w-3.5" })
108524
+ className: "absolute right-2 top-2 h-7 w-7 bg-background/80 opacity-0 shadow-sm backdrop-blur transition-opacity hover:bg-muted group-hover/code:opacity-100 focus-visible:opacity-100"
108293
108525
  });
108294
108526
  }
108295
108527
  (0, import_react.useEffect)(() => {
@@ -124600,7 +124832,6 @@ function FileContent({ orchestratorId, selectedPath, line, onReadError }) {
124600
124832
  const { file, loading, error } = useFileRead(orchestratorId, selectedPath);
124601
124833
  const lineRef = (0, import_react.useRef)(null);
124602
124834
  const [mode, setMode] = (0, import_react.useState)("raw");
124603
- const [copiedPath, setCopiedPath] = (0, import_react.useState)(false);
124604
124835
  (0, import_react.useEffect)(() => {
124605
124836
  onReadError(error);
124606
124837
  }, [error, onReadError]);
@@ -124622,15 +124853,6 @@ function FileContent({ orchestratorId, selectedPath, line, onReadError }) {
124622
124853
  file?.content,
124623
124854
  line
124624
124855
  ]);
124625
- async function copyPath(path) {
124626
- try {
124627
- await navigator.clipboard?.writeText(path);
124628
- setCopiedPath(true);
124629
- window.setTimeout(() => setCopiedPath(false), 1400);
124630
- } catch {
124631
- setCopiedPath(false);
124632
- }
124633
- }
124634
124856
  function selectMode(nextMode, kind) {
124635
124857
  setMode(nextMode);
124636
124858
  if (kind) writeModePreference(kind, nextMode);
@@ -124655,14 +124877,13 @@ function FileContent({ orchestratorId, selectedPath, line, onReadError }) {
124655
124877
  className: "min-w-0 flex-1 truncate font-mono",
124656
124878
  children: file.path
124657
124879
  }),
124658
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
124659
- type: "button",
124880
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CopyButton, {
124881
+ value: file.path,
124882
+ label: "Copy path",
124883
+ copiedLabel: "Copied path",
124660
124884
  size: "icon",
124661
124885
  variant: "ghost",
124662
- className: "h-7 w-7 shrink-0",
124663
- onClick: () => copyPath(file.path),
124664
- title: copiedPath ? "Copied path" : "Copy path",
124665
- children: copiedPath ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Check, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Copy, { className: "h-3.5 w-3.5" })
124886
+ className: "h-7 w-7 shrink-0"
124666
124887
  }),
124667
124888
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge$1, {
124668
124889
  variant: "outline",
@@ -124719,14 +124940,13 @@ function FileContent({ orchestratorId, selectedPath, line, onReadError }) {
124719
124940
  className: "min-w-0 flex-1 truncate font-mono",
124720
124941
  children: file.path
124721
124942
  }),
124722
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
124723
- type: "button",
124943
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CopyButton, {
124944
+ value: file.path,
124945
+ label: "Copy path",
124946
+ copiedLabel: "Copied path",
124724
124947
  size: "icon",
124725
124948
  variant: "ghost",
124726
- className: "h-7 w-7 shrink-0",
124727
- onClick: () => copyPath(file.path),
124728
- title: copiedPath ? "Copied path" : "Copy path",
124729
- children: copiedPath ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Check, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Copy, { className: "h-3.5 w-3.5" })
124949
+ className: "h-7 w-7 shrink-0"
124730
124950
  }),
124731
124951
  file.truncated && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge$1, {
124732
124952
  variant: "secondary",
@@ -126475,9 +126695,14 @@ function AddReaction({ open, onToggle, onReact }) {
126475
126695
  })]
126476
126696
  });
126477
126697
  }
126698
+ function useTtsPlayingKey() {
126699
+ return (0, import_react.useSyncExternalStore)((cb) => voiceTts.subscribe(cb), () => voiceTts.getPlayingKey(), () => null);
126700
+ }
126478
126701
  var MessageBubble = (0, import_react.memo)(function MessageBubble({ msg, peer, onOpenReferencedPath, onPreviewReferencedPath, onPreviewReferencedPathEnd }) {
126479
126702
  const isOutbound = msg.from === HUMAN_AGENT_ID;
126480
126703
  const reactToMessage = useRelayStore((s) => s.reactToMessage);
126704
+ const voiceTtsEnabled = useRelayStore((s) => s.voiceTtsEnabled);
126705
+ const ttsPlayingKey = useTtsPlayingKey();
126481
126706
  const peerCwd = useRelayStore((s) => {
126482
126707
  const cwd = s.agentsById[peer]?.meta?.cwd;
126483
126708
  return typeof cwd === "string" ? cwd : "";
@@ -126492,6 +126717,13 @@ var MessageBubble = (0, import_react.memo)(function MessageBubble({ msg, peer, o
126492
126717
  const reactions = groupedReactions(msg);
126493
126718
  const receipt = isOutbound ? outboundReceipt(msg, peer) : null;
126494
126719
  const ReceiptIcon = receipt?.icon;
126720
+ const isTtsPlaying = ttsPlayingKey === String(msg.id);
126721
+ const showPlayButton = voiceTtsEnabled && !isOutbound && !isReactionEvent(msg) && body.trim().length > 0;
126722
+ function togglePlay(e) {
126723
+ e.stopPropagation();
126724
+ if (isTtsPlaying) voiceTts.bargeIn();
126725
+ else voiceTts.speak(msg.body, String(msg.id));
126726
+ }
126495
126727
  (0, import_react.useEffect)(() => {
126496
126728
  if (!showQuickReact) return;
126497
126729
  function dismiss(e) {
@@ -126543,11 +126775,19 @@ var MessageBubble = (0, import_react.memo)(function MessageBubble({ msg, peer, o
126543
126775
  setShowQuickReact((v) => !v);
126544
126776
  }
126545
126777
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
126778
+ "data-msg-id": msg.id,
126546
126779
  className: cn$2("group/msg flex mb-3", isOutbound ? "justify-end" : "justify-start"),
126547
126780
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
126548
126781
  ref: bubbleRef,
126549
126782
  className: "relative max-w-[85%] md:max-w-[75%]",
126550
126783
  children: [
126784
+ showPlayButton && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
126785
+ type: "button",
126786
+ title: isTtsPlaying ? "Stop playback" : "Play aloud",
126787
+ onClick: togglePlay,
126788
+ 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"),
126789
+ 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" })
126790
+ }),
126551
126791
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
126552
126792
  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"),
126553
126793
  onPointerDown: handleBubblePointerDown,
@@ -126778,6 +127018,109 @@ function sameActivityTurn(a, b) {
126778
127018
  if (at && bt) return at === bt;
126779
127019
  return !at && !bt;
126780
127020
  }
127021
+ function StickyPromptBanner({ scrollRef, timeline }) {
127022
+ const [hidden, setHidden] = (0, import_react.useState)(true);
127023
+ const [expanded, setExpanded] = (0, import_react.useState)(false);
127024
+ const [collapsed, setCollapsed] = (0, import_react.useState)(false);
127025
+ const [promptAbove, setPromptAbove] = (0, import_react.useState)(true);
127026
+ const lastOutbound = (0, import_react.useMemo)(() => {
127027
+ for (let i = timeline.length - 1; i >= 0; i--) {
127028
+ const entry = timeline[i];
127029
+ if (!entry || entry.type !== "message") continue;
127030
+ if (entry.msg.from === "user") return {
127031
+ id: entry.msg.id,
127032
+ body: messageBody(entry.msg)
127033
+ };
127034
+ }
127035
+ return null;
127036
+ }, [timeline]);
127037
+ (0, import_react.useEffect)(() => {
127038
+ const scrollEl = scrollRef.current;
127039
+ if (!lastOutbound || !scrollEl) {
127040
+ setHidden(true);
127041
+ return;
127042
+ }
127043
+ const target = scrollEl.querySelector(`[data-msg-id="${lastOutbound.id}"]`);
127044
+ if (!target) {
127045
+ setHidden(true);
127046
+ return;
127047
+ }
127048
+ const observer = new IntersectionObserver(([e]) => {
127049
+ if (!e) return;
127050
+ setHidden(e.isIntersecting);
127051
+ if (!e.isIntersecting) {
127052
+ const rootTop = e.rootBounds?.top ?? scrollEl.getBoundingClientRect().top;
127053
+ setPromptAbove(e.boundingClientRect.top < rootTop);
127054
+ }
127055
+ }, {
127056
+ root: scrollEl,
127057
+ threshold: 0
127058
+ });
127059
+ observer.observe(target);
127060
+ const onScroll = () => {
127061
+ const r = target.getBoundingClientRect();
127062
+ const sr = scrollEl.getBoundingClientRect();
127063
+ setPromptAbove(r.top < sr.top);
127064
+ };
127065
+ scrollEl.addEventListener("scroll", onScroll, { passive: true });
127066
+ return () => {
127067
+ observer.disconnect();
127068
+ scrollEl.removeEventListener("scroll", onScroll);
127069
+ };
127070
+ }, [lastOutbound?.id, scrollRef]);
127071
+ (0, import_react.useEffect)(() => {
127072
+ setExpanded(false);
127073
+ setCollapsed(false);
127074
+ }, [lastOutbound?.id]);
127075
+ if (hidden || !lastOutbound || !lastOutbound.body.trim()) return null;
127076
+ function scrollToPrompt() {
127077
+ const scrollEl = scrollRef.current;
127078
+ if (!scrollEl || !lastOutbound) return;
127079
+ scrollEl.querySelector(`[data-msg-id="${lastOutbound.id}"]`)?.scrollIntoView({
127080
+ behavior: "smooth",
127081
+ block: "center"
127082
+ });
127083
+ }
127084
+ const arrow = promptAbove ? "↑" : "↓";
127085
+ if (collapsed) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
127086
+ className: "sticky top-0 z-10 -mx-3 md:-mx-4 -mt-3 md:-mt-4 bg-background",
127087
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
127088
+ type: "button",
127089
+ 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",
127090
+ onClick: () => setCollapsed(false),
127091
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "Your prompt" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronDown, { className: "w-3 h-3" })]
127092
+ })
127093
+ });
127094
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
127095
+ 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",
127096
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
127097
+ className: "flex items-center gap-2 text-[11px] text-muted-foreground mb-0.5",
127098
+ children: [
127099
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
127100
+ className: "font-medium text-primary/70",
127101
+ children: "Your prompt"
127102
+ }),
127103
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
127104
+ type: "button",
127105
+ className: "ml-auto hover:text-foreground transition-colors",
127106
+ onClick: scrollToPrompt,
127107
+ children: [arrow, " scroll to original"]
127108
+ }),
127109
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
127110
+ type: "button",
127111
+ className: "hover:text-foreground transition-colors",
127112
+ onClick: () => setCollapsed(true),
127113
+ title: "Minimize",
127114
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronUp, { className: "w-3 h-3" })
127115
+ })
127116
+ ]
127117
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
127118
+ className: cn$2("text-sm text-foreground leading-snug cursor-pointer", !expanded && "line-clamp-2"),
127119
+ onClick: () => setExpanded((e) => !e),
127120
+ children: lastOutbound.body
127121
+ })]
127122
+ });
127123
+ }
126781
127124
  function ChatPanel({ threads, onBack, showBackButton }) {
126782
127125
  const selectedInboxThread = useRelayStore((s) => s.selectedInboxThread);
126783
127126
  const agentsById = useRelayStore((s) => s.agentsById);
@@ -127232,6 +127575,11 @@ function ChatPanel({ threads, onBack, showBackButton }) {
127232
127575
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
127233
127576
  className: "flex items-center gap-0.5 md:gap-1 shrink-0",
127234
127577
  children: agent && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
127578
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CopyButton, {
127579
+ value: agent.id,
127580
+ label: "Copy agent ID",
127581
+ size: "icon-sm"
127582
+ }),
127235
127583
  voiceTts.available && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
127236
127584
  variant: "ghost",
127237
127585
  size: "icon-sm",
@@ -127438,6 +127786,10 @@ function ChatPanel({ threads, onBack, showBackButton }) {
127438
127786
  children: "No messages yet"
127439
127787
  })]
127440
127788
  }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
127789
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StickyPromptBanner, {
127790
+ scrollRef: pinnedScroll.ref,
127791
+ timeline
127792
+ }),
127441
127793
  timeline.map((entry) => {
127442
127794
  if (entry.type === "message") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MessageBubble, {
127443
127795
  msg: entry.msg,
@@ -127872,6 +128224,13 @@ function AgentCard({ agent }) {
127872
128224
  className: "flex gap-1 mt-2.5 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity",
127873
128225
  onClick: (e) => e.stopPropagation(),
127874
128226
  children: [
128227
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CopyButton, {
128228
+ value: agent.id,
128229
+ label: "Copy agent ID",
128230
+ size: "icon",
128231
+ className: "h-7 w-7",
128232
+ iconClassName: "w-3 h-3"
128233
+ }),
127875
128234
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
127876
128235
  size: "icon",
127877
128236
  variant: "ghost",
@@ -129071,9 +129430,6 @@ function WorkspaceActions({ workspace, expanded, onToggleDetails }) {
129071
129430
  const gitState = useRelayStore((s) => s.workspaceGitState[workspace.id]);
129072
129431
  const landed = !!gitState && gitState.available !== false && gitState.landed === true;
129073
129432
  const mergeable = workspace.mode === "isolated" && Boolean(workspace.worktreePath) && MERGEABLE_STATUSES.has(workspace.status) && !landed;
129074
- async function copyPath() {
129075
- await navigator.clipboard?.writeText(openPath);
129076
- }
129077
129433
  async function merge() {
129078
129434
  await fetchWorkspaceMergePreview(workspace.id);
129079
129435
  await workspaceAction(workspace.id, "merge");
@@ -129096,13 +129452,13 @@ function WorkspaceActions({ workspace, expanded, onToggleDetails }) {
129096
129452
  onClick: () => void openFilesAt({ path: openPath }),
129097
129453
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FolderOpen, { className: "h-3.5 w-3.5" })
129098
129454
  }),
129099
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
129455
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CopyButton, {
129456
+ value: openPath,
129457
+ label: "Copy path",
129458
+ copiedLabel: "Copied path",
129100
129459
  size: "icon-sm",
129101
129460
  variant: "ghost",
129102
- title: "Copy path",
129103
- disabled: !openPath,
129104
- onClick: () => void copyPath(),
129105
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Copy, { className: "h-3.5 w-3.5" })
129461
+ disabled: !openPath
129106
129462
  }),
129107
129463
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
129108
129464
  size: "icon-sm",
@@ -129872,7 +130228,7 @@ function ChannelCard({ channel }) {
129872
130228
  }
129873
130229
  function ChannelsView() {
129874
130230
  const channels = useRelayStore((s) => s.channels);
129875
- const readyCount = channels.filter((c) => c.status !== "offline" && c.ready).length;
130231
+ const readyCount = channels.filter(channelIsReady).length;
129876
130232
  const errorCount = channels.filter((c) => c.targetHealth?.status === "error").length;
129877
130233
  const warningCount = channels.filter((c) => c.targetHealth?.status === "warning").length;
129878
130234
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
@@ -130382,10 +130738,6 @@ function SecurityView() {
130382
130738
  await api("POST", `/tokens/${encodeURIComponent(token.jti)}/revoke`);
130383
130739
  await refresh();
130384
130740
  }
130385
- async function copy(value) {
130386
- await navigator.clipboard?.writeText(value);
130387
- setStatus("Copied");
130388
- }
130389
130741
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
130390
130742
  className: "space-y-4",
130391
130743
  children: [
@@ -130615,11 +130967,12 @@ function SecurityView() {
130615
130967
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
130616
130968
  className: "text-xs text-muted-foreground",
130617
130969
  children: "New token"
130618
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
130619
- variant: "ghost",
130970
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CopyButton, {
130971
+ value: issuedToken,
130972
+ label: "Copy token",
130620
130973
  size: "sm",
130621
- onClick: () => void copy(issuedToken),
130622
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Copy, { className: "h-3.5 w-3.5" })
130974
+ variant: "ghost",
130975
+ onCopied: () => setStatus("Copied")
130623
130976
  })]
130624
130977
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", {
130625
130978
  className: "block max-h-24 overflow-auto break-all text-xs",
@@ -153311,8 +153664,11 @@ function MaintenanceView() {
153311
153664
  })]
153312
153665
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ScrollArea, {
153313
153666
  className: "h-[calc(100dvh-10rem)]",
153314
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153315
- className: "overflow-x-auto rounded-md border border-border",
153667
+ children: jobs.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153668
+ className: "rounded-md border border-border px-3 py-12 text-center text-sm text-muted-foreground",
153669
+ children: "No maintenance jobs registered"
153670
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153671
+ className: "hidden overflow-x-auto rounded-md border border-border md:block",
153316
153672
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", {
153317
153673
  className: "w-full min-w-[980px] text-sm",
153318
153674
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", {
@@ -153347,22 +153703,117 @@ function MaintenanceView() {
153347
153703
  children: "Action"
153348
153704
  })
153349
153705
  ] })
153350
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: jobs.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
153351
- colSpan: 7,
153352
- className: "px-3 py-12 text-center text-sm text-muted-foreground",
153353
- children: "No maintenance jobs registered"
153354
- }) }) : jobs.map((job) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MaintenanceRow, {
153706
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: jobs.map((job) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MaintenanceRow, {
153355
153707
  job,
153356
153708
  now,
153357
153709
  onRun: () => void runMaintenanceJob(job.id)
153358
153710
  }, job.id)) })]
153359
153711
  })
153360
- })
153712
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153713
+ className: "space-y-3 md:hidden",
153714
+ children: jobs.map((job) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MaintenanceCard, {
153715
+ job,
153716
+ now,
153717
+ onRun: () => void runMaintenanceJob(job.id)
153718
+ }, job.id))
153719
+ })] })
153361
153720
  })]
153362
153721
  });
153363
153722
  }
153723
+ function jobStatus(job) {
153724
+ return job.running ? "running" : job.enabled ? job.lastStatus : "disabled";
153725
+ }
153726
+ function StatusBadge({ status }) {
153727
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Badge$1, {
153728
+ variant: "outline",
153729
+ className: cn$2("border", STATUS_CLASS[status] || STATUS_CLASS.idle),
153730
+ children: [statusIcon(status), status]
153731
+ });
153732
+ }
153733
+ function MaintenanceCard({ job, now, onRun }) {
153734
+ const status = jobStatus(job);
153735
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
153736
+ className: "rounded-md border border-border p-3",
153737
+ children: [
153738
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
153739
+ className: "flex items-start justify-between gap-2",
153740
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
153741
+ className: "min-w-0",
153742
+ children: [
153743
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153744
+ className: "font-medium",
153745
+ children: job.title
153746
+ }),
153747
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153748
+ className: "mt-0.5 text-xs text-muted-foreground",
153749
+ children: job.description
153750
+ }),
153751
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153752
+ className: "mt-1 font-mono text-[11px] text-muted-foreground/80",
153753
+ children: job.id
153754
+ })
153755
+ ]
153756
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusBadge, { status })]
153757
+ }),
153758
+ job.consecutiveFailures > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
153759
+ className: "mt-2 text-xs text-red-400",
153760
+ children: [
153761
+ job.consecutiveFailures,
153762
+ " failure",
153763
+ job.consecutiveFailures === 1 ? "" : "s"
153764
+ ]
153765
+ }),
153766
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("dl", {
153767
+ className: "mt-3 grid grid-cols-3 gap-2 text-xs",
153768
+ children: [
153769
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
153770
+ className: "text-muted-foreground",
153771
+ children: "Last run"
153772
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", { children: job.lastRunAt ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
153773
+ title: fmtTime$1(job.lastRunAt),
153774
+ children: timeAgo(now, job.lastRunAt)
153775
+ }) : "never" })] }),
153776
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
153777
+ className: "text-muted-foreground",
153778
+ children: "Next run"
153779
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", { children: job.nextRunAt ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
153780
+ title: fmtTime$1(job.nextRunAt),
153781
+ children: nextRunText(now, job.nextRunAt)
153782
+ }) : "-" })] }),
153783
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
153784
+ className: "text-muted-foreground",
153785
+ children: "Duration"
153786
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", { children: job.lastDurationMs !== void 0 ? `${job.lastDurationMs}ms` : "-" })] })
153787
+ ]
153788
+ }),
153789
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
153790
+ className: "mt-3",
153791
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153792
+ className: "text-xs text-muted-foreground",
153793
+ children: "Result"
153794
+ }), job.lastError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153795
+ className: "mt-0.5 text-xs text-red-400 line-clamp-3",
153796
+ children: job.lastError
153797
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153798
+ className: "mt-0.5 font-mono text-[11px] text-muted-foreground line-clamp-3",
153799
+ children: resultSummary(job)
153800
+ })]
153801
+ }),
153802
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153803
+ className: "mt-3 flex justify-end",
153804
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
153805
+ size: "sm",
153806
+ variant: "outline",
153807
+ disabled: !job.enabled || job.running,
153808
+ onClick: onRun,
153809
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Play, { className: "h-3.5 w-3.5" }), " Run"]
153810
+ })
153811
+ })
153812
+ ]
153813
+ });
153814
+ }
153364
153815
  function MaintenanceRow({ job, now, onRun }) {
153365
- const status = job.running ? "running" : job.enabled ? job.lastStatus : "disabled";
153816
+ const status = jobStatus(job);
153366
153817
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", {
153367
153818
  className: "border-t border-border align-top",
153368
153819
  children: [
@@ -153385,11 +153836,7 @@ function MaintenanceRow({ job, now, onRun }) {
153385
153836
  }),
153386
153837
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("td", {
153387
153838
  className: "px-3 py-3",
153388
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Badge$1, {
153389
- variant: "outline",
153390
- className: cn$2("border", STATUS_CLASS[status] || STATUS_CLASS.idle),
153391
- children: [statusIcon(status), status]
153392
- }), job.consecutiveFailures > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
153839
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusBadge, { status }), job.consecutiveFailures > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
153393
153840
  className: "mt-1 text-xs text-red-400",
153394
153841
  children: [
153395
153842
  job.consecutiveFailures,
@@ -154071,7 +154518,6 @@ function AgentDiagnostics({ agent, orchestrators }) {
154071
154518
  const now = useNow();
154072
154519
  const [policyHealth, setPolicyHealth] = (0, import_react.useState)(null);
154073
154520
  const [agentEvents, setAgentEvents] = (0, import_react.useState)([]);
154074
- const [copied, setCopied] = (0, import_react.useState)(false);
154075
154521
  const [expandedSections, setExpandedSections] = (0, import_react.useState)({
154076
154522
  spawn: true,
154077
154523
  workspace: true,
@@ -154115,7 +154561,7 @@ function AgentDiagnostics({ agent, orchestrators }) {
154115
154561
  [key]: !s[key]
154116
154562
  }));
154117
154563
  }
154118
- async function copyDiagnosticBundle() {
154564
+ function buildDiagnosticBundle() {
154119
154565
  const policy = policyHealth?.policy;
154120
154566
  const state = policyHealth?.state;
154121
154567
  const lines = [
@@ -154192,11 +154638,7 @@ function AgentDiagnostics({ agent, orchestrators }) {
154192
154638
  }
154193
154639
  const contracts = agent.meta?.contracts;
154194
154640
  if (contracts) for (const [k, v] of Object.entries(contracts)) lines.push(`Contract ${k}: ${v}`);
154195
- try {
154196
- await navigator.clipboard.writeText(lines.join("\n"));
154197
- setCopied(true);
154198
- setTimeout(() => setCopied(false), 2e3);
154199
- } catch {}
154641
+ return lines.join("\n");
154200
154642
  }
154201
154643
  const policy = policyHealth?.policy;
154202
154644
  const state = policyHealth?.state;
@@ -154208,12 +154650,14 @@ function AgentDiagnostics({ agent, orchestrators }) {
154208
154650
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h3", {
154209
154651
  className: "text-sm font-medium flex items-center gap-1.5",
154210
154652
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Stethoscope, { className: "w-3.5 h-3.5 text-muted-foreground" }), "Diagnostics"]
154211
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
154653
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CopyButton, {
154654
+ value: buildDiagnosticBundle,
154655
+ label: "Copy Bundle",
154656
+ showText: true,
154212
154657
  size: "sm",
154213
154658
  variant: "outline",
154214
154659
  className: "h-7 text-xs gap-1",
154215
- onClick: copyDiagnosticBundle,
154216
- children: [copied ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Check, { className: "w-3 h-3" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Copy, { className: "w-3 h-3" }), copied ? "Copied" : "Copy Bundle"]
154660
+ iconClassName: "w-3 h-3"
154217
154661
  })]
154218
154662
  }),
154219
154663
  policy && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CollapsibleSection, {
@@ -155406,13 +155850,21 @@ function AgentDetailDrawer() {
155406
155850
  className: "space-y-1 text-sm",
155407
155851
  children: [
155408
155852
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
155409
- className: "flex justify-between gap-2 min-w-0",
155853
+ className: "flex items-center justify-between gap-2 min-w-0",
155410
155854
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
155411
155855
  className: "text-muted-foreground shrink-0",
155412
155856
  children: "ID"
155413
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
155414
- className: "font-mono text-xs truncate",
155415
- children: agent.id
155857
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
155858
+ className: "flex min-w-0 items-center gap-1",
155859
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
155860
+ className: "font-mono text-xs truncate",
155861
+ children: agent.id
155862
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CopyButton, {
155863
+ value: agent.id,
155864
+ label: "Copy agent ID",
155865
+ size: "icon-xs",
155866
+ className: "shrink-0 text-muted-foreground"
155867
+ })]
155416
155868
  })]
155417
155869
  }),
155418
155870
  runtimePackage && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
@@ -156996,11 +157448,6 @@ function OrchestratorInstallModal() {
156996
157448
  setLoading(false);
156997
157449
  }
156998
157450
  }
156999
- async function copy() {
157000
- if (!command) return;
157001
- await navigator.clipboard.writeText(command);
157002
- showNotification("Install command copied");
157003
- }
157004
157451
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Dialog, {
157005
157452
  open,
157006
157453
  onOpenChange: (o) => !o && set({ orchestratorInstallOpen: false }),
@@ -157069,11 +157516,14 @@ function OrchestratorInstallModal() {
157069
157516
  onClick: () => set({ orchestratorInstallOpen: false }),
157070
157517
  children: "Close"
157071
157518
  }),
157072
- command && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
157519
+ command && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CopyButton, {
157520
+ value: command,
157521
+ label: "Copy",
157522
+ showText: true,
157523
+ size: "default",
157073
157524
  variant: "outline",
157074
157525
  className: "gap-1",
157075
- onClick: copy,
157076
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Copy, { className: "w-3.5 h-3.5" }), "Copy"]
157526
+ onCopied: () => showNotification("Install command copied")
157077
157527
  }),
157078
157528
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
157079
157529
  className: "gap-1",
@@ -157589,6 +158039,29 @@ function AgentProfileModal() {
157589
158039
  ...partial
157590
158040
  } });
157591
158041
  }
158042
+ function updateCodexToolOutputTokenLimit(value) {
158043
+ const providerOptions = profile.providerOptions || {};
158044
+ const codex = { ...providerOptions.codex && typeof providerOptions.codex === "object" && !Array.isArray(providerOptions.codex) ? providerOptions.codex : {} };
158045
+ if (!value.trim()) delete codex.toolOutputTokenLimit;
158046
+ else {
158047
+ const limit = Number(value);
158048
+ if (!Number.isSafeInteger(limit)) return;
158049
+ codex.toolOutputTokenLimit = limit;
158050
+ }
158051
+ updateProfileModal({ providerOptions: {
158052
+ ...providerOptions,
158053
+ codex
158054
+ } });
158055
+ }
158056
+ function updateMaxSpawnedAgents(value) {
158057
+ if (!value.trim()) {
158058
+ updateProfileModal({ maxSpawnedAgents: void 0 });
158059
+ return;
158060
+ }
158061
+ const n = Number(value);
158062
+ if (!Number.isInteger(n) || n < 0) return;
158063
+ updateProfileModal({ maxSpawnedAgents: Math.min(n, 100) });
158064
+ }
157592
158065
  function selectBase(base) {
157593
158066
  const isHost = base === "host";
157594
158067
  updateProfileModal({
@@ -157615,6 +158088,8 @@ function AgentProfileModal() {
157615
158088
  }
157616
158089
  });
157617
158090
  }
158091
+ const codexOptions = profile.providerOptions?.codex && typeof profile.providerOptions.codex === "object" && !Array.isArray(profile.providerOptions.codex) ? profile.providerOptions.codex : {};
158092
+ const codexToolOutputTokenLimit = Number.isSafeInteger(codexOptions.toolOutputTokenLimit) ? String(codexOptions.toolOutputTokenLimit) : "";
157618
158093
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Dialog, {
157619
158094
  open,
157620
158095
  onOpenChange: (o) => !o && closeProfileModal(),
@@ -157752,6 +158227,27 @@ function AgentProfileModal() {
157752
158227
  })] })]
157753
158228
  })
157754
158229
  }),
158230
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Section, {
158231
+ title: "Spawning",
158232
+ defaultOpen: false,
158233
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
158234
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, { children: "Spawn Quota" }),
158235
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
158236
+ type: "number",
158237
+ min: 0,
158238
+ max: 100,
158239
+ step: 1,
158240
+ value: profile.maxSpawnedAgents === void 0 ? "" : String(profile.maxSpawnedAgents),
158241
+ onChange: (e) => updateMaxSpawnedAgents(e.target.value),
158242
+ placeholder: "0",
158243
+ disabled: readOnly
158244
+ }),
158245
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
158246
+ className: "mt-1 text-xs text-muted-foreground",
158247
+ children: "Max concurrent live child agents this profile may spawn. Empty or 0 = cannot spawn (default). Children never inherit it (no grandchildren)."
158248
+ })
158249
+ ] })
158250
+ }),
157755
158251
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Section, {
157756
158252
  title: "Environment Variables",
157757
158253
  defaultOpen: false,
@@ -157761,10 +158257,18 @@ function AgentProfileModal() {
157761
158257
  disabled: readOnly
157762
158258
  })
157763
158259
  }),
157764
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Section, {
158260
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Section, {
157765
158261
  title: "Provider Options",
157766
158262
  defaultOpen: false,
157767
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Textarea, {
158263
+ 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, {
158264
+ type: "number",
158265
+ min: 1e3,
158266
+ max: 2e5,
158267
+ value: codexToolOutputTokenLimit,
158268
+ onChange: (e) => updateCodexToolOutputTokenLimit(e.target.value),
158269
+ placeholder: "12000",
158270
+ disabled: readOnly
158271
+ })] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Textarea, {
157768
158272
  value: JSON.stringify(profile.providerOptions || {}, null, 2),
157769
158273
  onChange: (e) => {
157770
158274
  try {
@@ -157775,7 +158279,7 @@ function AgentProfileModal() {
157775
158279
  disabled: readOnly,
157776
158280
  className: "font-mono text-xs",
157777
158281
  placeholder: "{}"
157778
- })
158282
+ })]
157779
158283
  })
157780
158284
  ]
157781
158285
  }),
@@ -158607,6 +159111,10 @@ if ("serviceWorker" in navigator) {
158607
159111
  inset-block: calc(var(--spacing) * 0);
158608
159112
  }
158609
159113
 
159114
+ .-top-2 {
159115
+ top: calc(var(--spacing) * -2);
159116
+ }
159117
+
158610
159118
  .top-0 {
158611
159119
  top: calc(var(--spacing) * 0);
158612
159120
  }
@@ -158635,6 +159143,10 @@ if ("serviceWorker" in navigator) {
158635
159143
  right: calc(var(--spacing) * -.5);
158636
159144
  }
158637
159145
 
159146
+ .-right-2 {
159147
+ right: calc(var(--spacing) * -2);
159148
+ }
159149
+
158638
159150
  .right-2 {
158639
159151
  right: calc(var(--spacing) * 2);
158640
159152
  }
@@ -158769,6 +159281,10 @@ if ("serviceWorker" in navigator) {
158769
159281
  margin-inline: calc(var(--spacing) * -1);
158770
159282
  }
158771
159283
 
159284
+ .-mx-3 {
159285
+ margin-inline: calc(var(--spacing) * -3);
159286
+ }
159287
+
158772
159288
  .-mx-4 {
158773
159289
  margin-inline: calc(var(--spacing) * -4);
158774
159290
  }
@@ -158793,6 +159309,10 @@ if ("serviceWorker" in navigator) {
158793
159309
  margin-top: calc(var(--spacing) * -1);
158794
159310
  }
158795
159311
 
159312
+ .-mt-3 {
159313
+ margin-top: calc(var(--spacing) * -3);
159314
+ }
159315
+
158796
159316
  .mt-0\.5 {
158797
159317
  margin-top: calc(var(--spacing) * .5);
158798
159318
  }
@@ -158931,6 +159451,10 @@ if ("serviceWorker" in navigator) {
158931
159451
  display: inline-flex;
158932
159452
  }
158933
159453
 
159454
+ .table {
159455
+ display: table;
159456
+ }
159457
+
158934
159458
  .field-sizing-content {
158935
159459
  field-sizing: content;
158936
159460
  }
@@ -160183,6 +160707,16 @@ if ("serviceWorker" in navigator) {
160183
160707
  }
160184
160708
  }
160185
160709
 
160710
+ .border-primary\/10 {
160711
+ border-color: var(--primary);
160712
+ }
160713
+
160714
+ @supports (color: color-mix(in lab, red, red)) {
160715
+ .border-primary\/10 {
160716
+ border-color: color-mix(in oklab, var(--primary) 10%, transparent);
160717
+ }
160718
+ }
160719
+
160186
160720
  .border-primary\/25 {
160187
160721
  border-color: var(--primary);
160188
160722
  }
@@ -161223,6 +161757,10 @@ if ("serviceWorker" in navigator) {
161223
161757
  padding-bottom: calc(var(--spacing) * 1);
161224
161758
  }
161225
161759
 
161760
+ .pb-1\.5 {
161761
+ padding-bottom: calc(var(--spacing) * 1.5);
161762
+ }
161763
+
161226
161764
  .pb-2 {
161227
161765
  padding-bottom: calc(var(--spacing) * 2);
161228
161766
  }
@@ -162440,6 +162978,10 @@ if ("serviceWorker" in navigator) {
162440
162978
  .hover\:opacity-80:hover {
162441
162979
  opacity: .8;
162442
162980
  }
162981
+
162982
+ .hover\:opacity-100:hover {
162983
+ opacity: 1;
162984
+ }
162443
162985
  }
162444
162986
 
162445
162987
  .focus\:bg-accent:focus {
@@ -163007,6 +163549,14 @@ if ("serviceWorker" in navigator) {
163007
163549
  margin: calc(var(--spacing) * -6);
163008
163550
  }
163009
163551
 
163552
+ .md\:-mx-4 {
163553
+ margin-inline: calc(var(--spacing) * -4);
163554
+ }
163555
+
163556
+ .md\:-mt-4 {
163557
+ margin-top: calc(var(--spacing) * -4);
163558
+ }
163559
+
163010
163560
  .md\:block {
163011
163561
  display: block;
163012
163562
  }
@@ -163094,6 +163644,10 @@ if ("serviceWorker" in navigator) {
163094
163644
  padding: calc(var(--spacing) * 6);
163095
163645
  }
163096
163646
 
163647
+ .md\:px-3 {
163648
+ padding-inline: calc(var(--spacing) * 3);
163649
+ }
163650
+
163097
163651
  .md\:px-4 {
163098
163652
  padding-inline: calc(var(--spacing) * 4);
163099
163653
  }
@@ -163115,6 +163669,10 @@ if ("serviceWorker" in navigator) {
163115
163669
  text-wrap: pretty;
163116
163670
  }
163117
163671
 
163672
+ .md\:opacity-0 {
163673
+ opacity: 0;
163674
+ }
163675
+
163118
163676
  @media (hover: hover) {
163119
163677
  .md\:group-hover\/msg\:pointer-events-auto:is(:where(.group\/msg):hover *) {
163120
163678
  pointer-events: auto;