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/docs/openapi.json +44 -8
- package/package.json +2 -2
- package/public/index.html +565 -136
- package/runner/src/adapter.ts +3 -1
- package/scripts/install-bin-shim.cjs +16 -3
- package/src/agent-ref.ts +217 -0
- package/src/automations.ts +4 -1
- package/src/cli.ts +8 -2
- package/src/config-store.ts +37 -1
- package/src/context-router.ts +7 -7
- package/src/db.ts +218 -29
- package/src/managed-policy.ts +4 -1
- package/src/mcp.ts +208 -67
- package/src/routes.ts +28 -3
- package/src/runtime-tokens.ts +26 -1
- package/src/security.ts +3 -1
- package/src/token-db.ts +3 -3
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
|
-
|
|
10194
|
-
|
|
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/
|
|
10351
|
+
//#region ../sdk/src/speech-text.ts
|
|
10334
10352
|
/**
|
|
10335
|
-
*
|
|
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
|
-
*
|
|
10338
|
-
*
|
|
10339
|
-
*
|
|
10340
|
-
*
|
|
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
|
-
*
|
|
10343
|
-
*
|
|
10344
|
-
*
|
|
10345
|
-
*
|
|
10346
|
-
*
|
|
10347
|
-
*
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
126884
|
-
|
|
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(
|
|
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
|
-
|
|
152499
|
-
|
|
152500
|
-
|
|
152501
|
-
|
|
152502
|
-
|
|
152503
|
-
|
|
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
|
|
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: () =>
|
|
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
|
|
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
|
-
|
|
152585
|
-
|
|
152586
|
-
|
|
152587
|
-
|
|
152588
|
-
const
|
|
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
|
-
|
|
152621
|
-
|
|
152622
|
-
|
|
152623
|
-
|
|
152624
|
-
|
|
152625
|
-
|
|
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
|
-
}, [
|
|
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] ??
|
|
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
|
-
}, [
|
|
152993
|
+
}, [data]);
|
|
152668
152994
|
const activityBreakdown = (0, import_react.useMemo)(() => {
|
|
152669
|
-
const counts =
|
|
152670
|
-
|
|
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
|
-
}, [
|
|
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 =
|
|
152819
|
-
const reactionCount =
|
|
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:
|
|
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.
|
|
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)(
|
|
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;
|