agent-relay-server 0.20.0 → 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/package.json +2 -2
- package/public/index.html +471 -46
- package/runner/src/adapter.ts +1 -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 +35 -1
- package/src/context-router.ts +7 -7
- package/src/db.ts +111 -29
- package/src/managed-policy.ts +4 -1
- package/src/mcp.ts +208 -67
- package/src/routes.ts +15 -3
- package/src/runtime-tokens.ts +26 -1
- package/src/security.ts +3 -1
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",
|
|
@@ -12732,15 +12926,15 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
12732
12926
|
if (event === "connected") return;
|
|
12733
12927
|
if (event === "message.new") {
|
|
12734
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
|
+
}
|
|
12735
12933
|
const s = get();
|
|
12736
12934
|
if (s.messages.some((m) => m.id === msg.id)) return;
|
|
12737
12935
|
const msgs = [...s.messages, msg];
|
|
12738
12936
|
if (msgs.length > 500) msgs.splice(0, msgs.length - 500);
|
|
12739
12937
|
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
12938
|
const peer = inboxPeer(msg);
|
|
12745
12939
|
if (isHumanInboundMessage(msg) && peer && s.view === "chat" && s.selectedInboxThread === peer && !isDashboardHidden()) get().markInboxThreadReadTo(peer, msg.id);
|
|
12746
12940
|
return;
|
|
@@ -126475,9 +126669,14 @@ function AddReaction({ open, onToggle, onReact }) {
|
|
|
126475
126669
|
})]
|
|
126476
126670
|
});
|
|
126477
126671
|
}
|
|
126672
|
+
function useTtsPlayingKey() {
|
|
126673
|
+
return (0, import_react.useSyncExternalStore)((cb) => voiceTts.subscribe(cb), () => voiceTts.getPlayingKey(), () => null);
|
|
126674
|
+
}
|
|
126478
126675
|
var MessageBubble = (0, import_react.memo)(function MessageBubble({ msg, peer, onOpenReferencedPath, onPreviewReferencedPath, onPreviewReferencedPathEnd }) {
|
|
126479
126676
|
const isOutbound = msg.from === HUMAN_AGENT_ID;
|
|
126480
126677
|
const reactToMessage = useRelayStore((s) => s.reactToMessage);
|
|
126678
|
+
const voiceTtsEnabled = useRelayStore((s) => s.voiceTtsEnabled);
|
|
126679
|
+
const ttsPlayingKey = useTtsPlayingKey();
|
|
126481
126680
|
const peerCwd = useRelayStore((s) => {
|
|
126482
126681
|
const cwd = s.agentsById[peer]?.meta?.cwd;
|
|
126483
126682
|
return typeof cwd === "string" ? cwd : "";
|
|
@@ -126492,6 +126691,13 @@ var MessageBubble = (0, import_react.memo)(function MessageBubble({ msg, peer, o
|
|
|
126492
126691
|
const reactions = groupedReactions(msg);
|
|
126493
126692
|
const receipt = isOutbound ? outboundReceipt(msg, peer) : null;
|
|
126494
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
|
+
}
|
|
126495
126701
|
(0, import_react.useEffect)(() => {
|
|
126496
126702
|
if (!showQuickReact) return;
|
|
126497
126703
|
function dismiss(e) {
|
|
@@ -126543,11 +126749,19 @@ var MessageBubble = (0, import_react.memo)(function MessageBubble({ msg, peer, o
|
|
|
126543
126749
|
setShowQuickReact((v) => !v);
|
|
126544
126750
|
}
|
|
126545
126751
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
126752
|
+
"data-msg-id": msg.id,
|
|
126546
126753
|
className: cn$2("group/msg flex mb-3", isOutbound ? "justify-end" : "justify-start"),
|
|
126547
126754
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
126548
126755
|
ref: bubbleRef,
|
|
126549
126756
|
className: "relative max-w-[85%] md:max-w-[75%]",
|
|
126550
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
|
+
}),
|
|
126551
126765
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
126552
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"),
|
|
126553
126767
|
onPointerDown: handleBubblePointerDown,
|
|
@@ -126778,6 +126992,109 @@ function sameActivityTurn(a, b) {
|
|
|
126778
126992
|
if (at && bt) return at === bt;
|
|
126779
126993
|
return !at && !bt;
|
|
126780
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
|
+
}
|
|
126781
127098
|
function ChatPanel({ threads, onBack, showBackButton }) {
|
|
126782
127099
|
const selectedInboxThread = useRelayStore((s) => s.selectedInboxThread);
|
|
126783
127100
|
const agentsById = useRelayStore((s) => s.agentsById);
|
|
@@ -127438,6 +127755,10 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
127438
127755
|
children: "No messages yet"
|
|
127439
127756
|
})]
|
|
127440
127757
|
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
127758
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(StickyPromptBanner, {
|
|
127759
|
+
scrollRef: pinnedScroll.ref,
|
|
127760
|
+
timeline
|
|
127761
|
+
}),
|
|
127441
127762
|
timeline.map((entry) => {
|
|
127442
127763
|
if (entry.type === "message") return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MessageBubble, {
|
|
127443
127764
|
msg: entry.msg,
|
|
@@ -129872,7 +130193,7 @@ function ChannelCard({ channel }) {
|
|
|
129872
130193
|
}
|
|
129873
130194
|
function ChannelsView() {
|
|
129874
130195
|
const channels = useRelayStore((s) => s.channels);
|
|
129875
|
-
const readyCount = channels.filter(
|
|
130196
|
+
const readyCount = channels.filter(channelIsReady).length;
|
|
129876
130197
|
const errorCount = channels.filter((c) => c.targetHealth?.status === "error").length;
|
|
129877
130198
|
const warningCount = channels.filter((c) => c.targetHealth?.status === "warning").length;
|
|
129878
130199
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
@@ -157589,6 +157910,29 @@ function AgentProfileModal() {
|
|
|
157589
157910
|
...partial
|
|
157590
157911
|
} });
|
|
157591
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
|
+
}
|
|
157592
157936
|
function selectBase(base) {
|
|
157593
157937
|
const isHost = base === "host";
|
|
157594
157938
|
updateProfileModal({
|
|
@@ -157615,6 +157959,8 @@ function AgentProfileModal() {
|
|
|
157615
157959
|
}
|
|
157616
157960
|
});
|
|
157617
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) : "";
|
|
157618
157964
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Dialog, {
|
|
157619
157965
|
open,
|
|
157620
157966
|
onOpenChange: (o) => !o && closeProfileModal(),
|
|
@@ -157752,6 +158098,27 @@ function AgentProfileModal() {
|
|
|
157752
158098
|
})] })]
|
|
157753
158099
|
})
|
|
157754
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
|
+
}),
|
|
157755
158122
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Section, {
|
|
157756
158123
|
title: "Environment Variables",
|
|
157757
158124
|
defaultOpen: false,
|
|
@@ -157761,10 +158128,18 @@ function AgentProfileModal() {
|
|
|
157761
158128
|
disabled: readOnly
|
|
157762
158129
|
})
|
|
157763
158130
|
}),
|
|
157764
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
158131
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Section, {
|
|
157765
158132
|
title: "Provider Options",
|
|
157766
158133
|
defaultOpen: false,
|
|
157767
|
-
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, {
|
|
157768
158143
|
value: JSON.stringify(profile.providerOptions || {}, null, 2),
|
|
157769
158144
|
onChange: (e) => {
|
|
157770
158145
|
try {
|
|
@@ -157775,7 +158150,7 @@ function AgentProfileModal() {
|
|
|
157775
158150
|
disabled: readOnly,
|
|
157776
158151
|
className: "font-mono text-xs",
|
|
157777
158152
|
placeholder: "{}"
|
|
157778
|
-
})
|
|
158153
|
+
})]
|
|
157779
158154
|
})
|
|
157780
158155
|
]
|
|
157781
158156
|
}),
|
|
@@ -158607,6 +158982,10 @@ if ("serviceWorker" in navigator) {
|
|
|
158607
158982
|
inset-block: calc(var(--spacing) * 0);
|
|
158608
158983
|
}
|
|
158609
158984
|
|
|
158985
|
+
.-top-2 {
|
|
158986
|
+
top: calc(var(--spacing) * -2);
|
|
158987
|
+
}
|
|
158988
|
+
|
|
158610
158989
|
.top-0 {
|
|
158611
158990
|
top: calc(var(--spacing) * 0);
|
|
158612
158991
|
}
|
|
@@ -158635,6 +159014,10 @@ if ("serviceWorker" in navigator) {
|
|
|
158635
159014
|
right: calc(var(--spacing) * -.5);
|
|
158636
159015
|
}
|
|
158637
159016
|
|
|
159017
|
+
.-right-2 {
|
|
159018
|
+
right: calc(var(--spacing) * -2);
|
|
159019
|
+
}
|
|
159020
|
+
|
|
158638
159021
|
.right-2 {
|
|
158639
159022
|
right: calc(var(--spacing) * 2);
|
|
158640
159023
|
}
|
|
@@ -158769,6 +159152,10 @@ if ("serviceWorker" in navigator) {
|
|
|
158769
159152
|
margin-inline: calc(var(--spacing) * -1);
|
|
158770
159153
|
}
|
|
158771
159154
|
|
|
159155
|
+
.-mx-3 {
|
|
159156
|
+
margin-inline: calc(var(--spacing) * -3);
|
|
159157
|
+
}
|
|
159158
|
+
|
|
158772
159159
|
.-mx-4 {
|
|
158773
159160
|
margin-inline: calc(var(--spacing) * -4);
|
|
158774
159161
|
}
|
|
@@ -158793,6 +159180,10 @@ if ("serviceWorker" in navigator) {
|
|
|
158793
159180
|
margin-top: calc(var(--spacing) * -1);
|
|
158794
159181
|
}
|
|
158795
159182
|
|
|
159183
|
+
.-mt-3 {
|
|
159184
|
+
margin-top: calc(var(--spacing) * -3);
|
|
159185
|
+
}
|
|
159186
|
+
|
|
158796
159187
|
.mt-0\.5 {
|
|
158797
159188
|
margin-top: calc(var(--spacing) * .5);
|
|
158798
159189
|
}
|
|
@@ -160183,6 +160574,16 @@ if ("serviceWorker" in navigator) {
|
|
|
160183
160574
|
}
|
|
160184
160575
|
}
|
|
160185
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
|
+
|
|
160186
160587
|
.border-primary\/25 {
|
|
160187
160588
|
border-color: var(--primary);
|
|
160188
160589
|
}
|
|
@@ -161223,6 +161624,10 @@ if ("serviceWorker" in navigator) {
|
|
|
161223
161624
|
padding-bottom: calc(var(--spacing) * 1);
|
|
161224
161625
|
}
|
|
161225
161626
|
|
|
161627
|
+
.pb-1\.5 {
|
|
161628
|
+
padding-bottom: calc(var(--spacing) * 1.5);
|
|
161629
|
+
}
|
|
161630
|
+
|
|
161226
161631
|
.pb-2 {
|
|
161227
161632
|
padding-bottom: calc(var(--spacing) * 2);
|
|
161228
161633
|
}
|
|
@@ -162440,6 +162845,10 @@ if ("serviceWorker" in navigator) {
|
|
|
162440
162845
|
.hover\:opacity-80:hover {
|
|
162441
162846
|
opacity: .8;
|
|
162442
162847
|
}
|
|
162848
|
+
|
|
162849
|
+
.hover\:opacity-100:hover {
|
|
162850
|
+
opacity: 1;
|
|
162851
|
+
}
|
|
162443
162852
|
}
|
|
162444
162853
|
|
|
162445
162854
|
.focus\:bg-accent:focus {
|
|
@@ -163007,6 +163416,14 @@ if ("serviceWorker" in navigator) {
|
|
|
163007
163416
|
margin: calc(var(--spacing) * -6);
|
|
163008
163417
|
}
|
|
163009
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
|
+
|
|
163010
163427
|
.md\:block {
|
|
163011
163428
|
display: block;
|
|
163012
163429
|
}
|
|
@@ -163094,6 +163511,10 @@ if ("serviceWorker" in navigator) {
|
|
|
163094
163511
|
padding: calc(var(--spacing) * 6);
|
|
163095
163512
|
}
|
|
163096
163513
|
|
|
163514
|
+
.md\:px-3 {
|
|
163515
|
+
padding-inline: calc(var(--spacing) * 3);
|
|
163516
|
+
}
|
|
163517
|
+
|
|
163097
163518
|
.md\:px-4 {
|
|
163098
163519
|
padding-inline: calc(var(--spacing) * 4);
|
|
163099
163520
|
}
|
|
@@ -163115,6 +163536,10 @@ if ("serviceWorker" in navigator) {
|
|
|
163115
163536
|
text-wrap: pretty;
|
|
163116
163537
|
}
|
|
163117
163538
|
|
|
163539
|
+
.md\:opacity-0 {
|
|
163540
|
+
opacity: 0;
|
|
163541
|
+
}
|
|
163542
|
+
|
|
163118
163543
|
@media (hover: hover) {
|
|
163119
163544
|
.md\:group-hover\/msg\:pointer-events-auto:is(:where(.group\/msg):hover *) {
|
|
163120
163545
|
pointer-events: auto;
|