agent-relay-server 0.12.4 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/public/index.html CHANGED
@@ -10286,6 +10286,20 @@ async function apiUpload(path, file, meta) {
10286
10286
  }
10287
10287
  return response.json();
10288
10288
  }
10289
+ async function apiPostAudio(path, blob) {
10290
+ const headers = { "Content-Type": blob.type || "application/octet-stream" };
10291
+ if (authToken) headers["X-Agent-Relay-Token"] = authToken;
10292
+ const response = await fetch(new URL("api" + path, baseUrl()), {
10293
+ method: "POST",
10294
+ headers,
10295
+ body: blob
10296
+ });
10297
+ if (!response.ok) {
10298
+ if (response.status === 401) throw makeError(401, "Authentication required");
10299
+ throw makeError(response.status, await responseErrorMessage(response));
10300
+ }
10301
+ return response.json();
10302
+ }
10289
10303
  async function apiBlob(path) {
10290
10304
  const opts = {
10291
10305
  method: "GET",
@@ -10301,6 +10315,206 @@ async function apiBlob(path) {
10301
10315
  return response.blob();
10302
10316
  }
10303
10317
  //#endregion
10318
+ //#region src/lib/voice.ts
10319
+ /**
10320
+ * Browser voice I/O for chat.
10321
+ *
10322
+ * TTS (output) is 100% browser-native via the Web Speech API — the store
10323
+ * already receives every agent response turn, so there is no backend round-trip.
10324
+ * STT (input) records the mic and posts the clip to the voice connector's
10325
+ * whisper endpoint through the relay proxy (single-origin, auth-gated).
10326
+ *
10327
+ * Speaker policy ("active chat owns the speaker"):
10328
+ * - One utterance at a time. Disabled => silent.
10329
+ * - Only the active chat speaks, and only responses that arrive while it is
10330
+ * active (no backlog replay).
10331
+ * - Switching away lets the current utterance finish but drops the now-background
10332
+ * chat's queued + future responses.
10333
+ * - The active chat preempts: if it speaks while a previous chat's audio lingers,
10334
+ * that audio is cancelled.
10335
+ */
10336
+ /** Collapse markdown/code into something worth hearing (mirrors the connector's text.ts). */
10337
+ function speechify(markdown) {
10338
+ if (!markdown) return "";
10339
+ let text = markdown.replace(/\r\n/g, "\n");
10340
+ text = text.replace(/```[^\n]*\n([\s\S]*?)```/g, (_m, body) => {
10341
+ const lines = body.replace(/\n+$/, "").split("\n").length;
10342
+ return ` code block, ${lines} ${lines === 1 ? "line" : "lines"}. `;
10343
+ });
10344
+ text = text.replace(/```[^\n]*/g, " code block. ");
10345
+ text = text.replace(/^#{1,6}\s+/gm, "");
10346
+ text = text.replace(/^\s*[-*+]\s+/gm, ". ");
10347
+ text = text.replace(/^\s*\d+\.\s+/gm, ". ");
10348
+ text = text.replace(/`([^`]+)`/g, "$1");
10349
+ text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
10350
+ text = text.replace(/(\*\*|__|\*|_)(.*?)\1/g, "$2");
10351
+ text = text.replace(/[ \t]+/g, " ");
10352
+ text = text.replace(/\n{2,}/g, ". ");
10353
+ text = text.replace(/\s*\.\s*\.\s*(\.\s*)+/g, ". ");
10354
+ return text.trim();
10355
+ }
10356
+ var MAX_CHUNK = 220;
10357
+ /** Split into utterance-sized chunks (sentence boundaries; hard-split very long runs). */
10358
+ function chunkForSpeech(text) {
10359
+ const sentences = text.match(/[^.!?]+[.!?]*\s*/g)?.map((s) => s.trim()).filter(Boolean) ?? [text];
10360
+ const out = [];
10361
+ for (const s of sentences) {
10362
+ if (s.length <= MAX_CHUNK) {
10363
+ out.push(s);
10364
+ continue;
10365
+ }
10366
+ let rest = s;
10367
+ while (rest.length > MAX_CHUNK) {
10368
+ let cut = rest.lastIndexOf(" ", MAX_CHUNK);
10369
+ if (cut <= 0) cut = MAX_CHUNK;
10370
+ out.push(rest.slice(0, cut).trim());
10371
+ rest = rest.slice(cut).trim();
10372
+ }
10373
+ if (rest) out.push(rest);
10374
+ }
10375
+ return out;
10376
+ }
10377
+ var synthAvailable = typeof window !== "undefined" && "speechSynthesis" in window;
10378
+ var VoiceTts = class {
10379
+ enabled = false;
10380
+ active = null;
10381
+ queue = [];
10382
+ currentChat = null;
10383
+ speaking = false;
10384
+ gen = 0;
10385
+ get available() {
10386
+ return synthAvailable;
10387
+ }
10388
+ isEnabled() {
10389
+ return this.enabled;
10390
+ }
10391
+ setEnabled(on) {
10392
+ if (on === this.enabled) return;
10393
+ this.enabled = on;
10394
+ if (!on) this.reset();
10395
+ }
10396
+ setActiveChat(chatId) {
10397
+ if (chatId === this.active) return;
10398
+ this.active = chatId;
10399
+ this.queue = this.queue.filter((q) => q.chatId === chatId);
10400
+ }
10401
+ /** A captured agent response turn arrived for `chatId`. */
10402
+ onResponse(chatId, rawText) {
10403
+ if (!this.enabled || !synthAvailable || !chatId || chatId !== this.active) return;
10404
+ const text = speechify(rawText);
10405
+ if (!text) return;
10406
+ if (this.speaking && this.currentChat && this.currentChat !== chatId) {
10407
+ this.queue = [];
10408
+ this.cancel();
10409
+ }
10410
+ this.queue.push({
10411
+ chatId,
10412
+ text
10413
+ });
10414
+ this.pump();
10415
+ }
10416
+ /** Cut all speech immediately (e.g. when the user starts talking). */
10417
+ bargeIn() {
10418
+ this.reset();
10419
+ }
10420
+ reset() {
10421
+ this.queue = [];
10422
+ this.cancel();
10423
+ }
10424
+ cancel() {
10425
+ this.gen++;
10426
+ this.currentChat = null;
10427
+ this.speaking = false;
10428
+ try {
10429
+ window.speechSynthesis.cancel();
10430
+ } catch {}
10431
+ }
10432
+ pump() {
10433
+ if (this.speaking) return;
10434
+ const item = this.queue.shift();
10435
+ if (!item) return;
10436
+ this.speaking = true;
10437
+ this.currentChat = item.chatId;
10438
+ const gen = ++this.gen;
10439
+ const chunks = chunkForSpeech(item.text);
10440
+ const speakAt = (i) => {
10441
+ if (gen !== this.gen) return;
10442
+ if (i >= chunks.length) {
10443
+ this.speaking = false;
10444
+ this.currentChat = null;
10445
+ this.pump();
10446
+ return;
10447
+ }
10448
+ const u = new SpeechSynthesisUtterance(chunks[i]);
10449
+ u.lang = navigator.language || "en-US";
10450
+ u.onend = () => speakAt(i + 1);
10451
+ u.onerror = () => speakAt(i + 1);
10452
+ window.speechSynthesis.speak(u);
10453
+ };
10454
+ speakAt(0);
10455
+ }
10456
+ };
10457
+ var voiceTts = new VoiceTts();
10458
+ var micAvailable = typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia && typeof window !== "undefined" && "MediaRecorder" in window;
10459
+ function pickMimeType() {
10460
+ for (const m of [
10461
+ "audio/webm;codecs=opus",
10462
+ "audio/webm",
10463
+ "audio/ogg;codecs=opus",
10464
+ "audio/mp4"
10465
+ ]) if (typeof MediaRecorder !== "undefined" && MediaRecorder.isTypeSupported?.(m)) return m;
10466
+ }
10467
+ /** Hold-to-talk mic recorder. start() on press, stop() on release returns the clip. */
10468
+ var PttRecorder = class {
10469
+ rec = null;
10470
+ chunks = [];
10471
+ stream = null;
10472
+ async start() {
10473
+ this.stream = await navigator.mediaDevices.getUserMedia({ audio: true });
10474
+ this.chunks = [];
10475
+ const mime = pickMimeType();
10476
+ this.rec = new MediaRecorder(this.stream, mime ? { mimeType: mime } : void 0);
10477
+ this.rec.ondataavailable = (e) => {
10478
+ if (e.data.size) this.chunks.push(e.data);
10479
+ };
10480
+ this.rec.start();
10481
+ }
10482
+ async stop() {
10483
+ const rec = this.rec;
10484
+ if (!rec) {
10485
+ this.teardown();
10486
+ return null;
10487
+ }
10488
+ const stopped = new Promise((resolve) => {
10489
+ rec.onstop = () => resolve();
10490
+ });
10491
+ try {
10492
+ rec.stop();
10493
+ } catch {}
10494
+ await stopped;
10495
+ const type = rec.mimeType || "audio/webm";
10496
+ const blob = new Blob(this.chunks, { type });
10497
+ this.teardown();
10498
+ return blob.size ? blob : null;
10499
+ }
10500
+ cancel() {
10501
+ try {
10502
+ this.rec?.stop();
10503
+ } catch {}
10504
+ this.teardown();
10505
+ }
10506
+ teardown() {
10507
+ this.stream?.getTracks().forEach((t) => t.stop());
10508
+ this.rec = null;
10509
+ this.stream = null;
10510
+ this.chunks = [];
10511
+ }
10512
+ };
10513
+ /** Send a recorded clip through the relay proxy to the voice connector's whisper STT. */
10514
+ async function transcribeClip(blob) {
10515
+ return ((await apiPostAudio("/connectors/voice/call/transcribe", blob))?.text ?? "").trim();
10516
+ }
10517
+ //#endregion
10304
10518
  //#region src/store/clock.ts
10305
10519
  var useClock = create$1((set, get) => ({
10306
10520
  now: Date.now(),
@@ -10474,6 +10688,11 @@ var NAV_ITEMS = [
10474
10688
  label: "Analytics",
10475
10689
  icon: "AreaChart"
10476
10690
  },
10691
+ {
10692
+ key: "insights",
10693
+ label: "Insights",
10694
+ icon: "Lightbulb"
10695
+ },
10477
10696
  {
10478
10697
  key: "maintenance",
10479
10698
  label: "Maintenance",
@@ -10701,6 +10920,11 @@ function inboxPeer(msg) {
10701
10920
  function isHumanInboundMessage(msg) {
10702
10921
  return msg.to === "user" && msg.from !== "user";
10703
10922
  }
10923
+ function isSessionActivityStep(msg) {
10924
+ if (msg.kind !== "session") return false;
10925
+ const type = (msg.payload?.session)?.type;
10926
+ return type === "reasoning" || type === "tool";
10927
+ }
10704
10928
  function isClaimableTaskWaiting(task) {
10705
10929
  return WAITING_TASK_STATUSES.has(task.status) && !task.claimedBy;
10706
10930
  }
@@ -11636,6 +11860,7 @@ var useRelayStore = create$1()(persist((set, get) => ({
11636
11860
  showOffline: false,
11637
11861
  showBuiltIns: false,
11638
11862
  autoRefresh: true,
11863
+ voiceTtsEnabled: false,
11639
11864
  agentSort: "status",
11640
11865
  agentSortDir: "asc",
11641
11866
  agentPresetFilter: "",
@@ -11823,6 +12048,10 @@ var useRelayStore = create$1()(persist((set, get) => ({
11823
12048
  _refreshTimer: null,
11824
12049
  _refreshInFlight: false,
11825
12050
  set: (partial) => set(partial),
12051
+ setVoiceTtsEnabled(on) {
12052
+ voiceTts.setEnabled(on);
12053
+ set({ voiceTtsEnabled: on });
12054
+ },
11826
12055
  async init() {
11827
12056
  if (!useRelayStore.persist.hasHydrated()) await new Promise((resolve) => {
11828
12057
  const unsub = useRelayStore.persist.onFinishHydration(() => {
@@ -11832,6 +12061,8 @@ var useRelayStore = create$1()(persist((set, get) => ({
11832
12061
  });
11833
12062
  const token = get().authToken;
11834
12063
  if (token) setAuthToken(token);
12064
+ voiceTts.setEnabled(get().voiceTtsEnabled);
12065
+ syncVoiceActiveChat(get());
11835
12066
  setUnauthorizedHandler(() => {
11836
12067
  if (!get().authNeeded) set({
11837
12068
  authNeeded: true,
@@ -12345,6 +12576,10 @@ var useRelayStore = create$1()(persist((set, get) => ({
12345
12576
  const msgs = [...s.messages, msg];
12346
12577
  if (msgs.length > 500) msgs.splice(0, msgs.length - 500);
12347
12578
  set({ messages: msgs });
12579
+ if (msg.kind === "session") {
12580
+ const sess = msg.payload?.session;
12581
+ if ((sess?.type ?? "response") === "response" && (sess?.origin ?? "provider") === "provider") voiceTts.onResponse(inboxPeer(msg), msg.body);
12582
+ }
12348
12583
  const peer = inboxPeer(msg);
12349
12584
  if (isHumanInboundMessage(msg) && peer && s.view === "chat" && s.selectedInboxThread === peer && !isDashboardHidden()) get().markInboxThreadReadTo(peer, msg.id);
12350
12585
  return;
@@ -13914,6 +14149,7 @@ var useRelayStore = create$1()(persist((set, get) => ({
13914
14149
  showOffline: state.showOffline,
13915
14150
  showBuiltIns: state.showBuiltIns,
13916
14151
  autoRefresh: state.autoRefresh,
14152
+ voiceTtsEnabled: state.voiceTtsEnabled,
13917
14153
  agentSort: state.agentSort,
13918
14154
  agentSortDir: state.agentSortDir,
13919
14155
  agentPresetFilter: state.agentPresetFilter,
@@ -13953,6 +14189,14 @@ var useRelayStore = create$1()(persist((set, get) => ({
13953
14189
  spawnWorkspaceMode: state.spawnWorkspaceMode
13954
14190
  })
13955
14191
  }));
14192
+ var _voiceActiveChat = null;
14193
+ function syncVoiceActiveChat(s) {
14194
+ const active = s.voiceTtsEnabled && s.view === "chat" ? s.selectedInboxThread || null : null;
14195
+ if (active === _voiceActiveChat) return;
14196
+ _voiceActiveChat = active;
14197
+ voiceTts.setActiveChat(active);
14198
+ }
14199
+ useRelayStore.subscribe(syncVoiceActiveChat);
13956
14200
  //#endregion
13957
14201
  //#region src/lib/themes.ts
13958
14202
  var THEMES = [
@@ -25055,7 +25299,7 @@ function useAgentAttention(agent) {
25055
25299
  const peerMessages = messages.filter((m) => inboxPeer(m) === agent.id && !isReactionEventMessage(m));
25056
25300
  const cursor = Number(readCursors[agent.id] || 0);
25057
25301
  const archivedAt = Number(archivedThreads[agent.id] || 0);
25058
- const unread = archivedAt >= peerMessages.filter(isHumanInboundMessage).reduce((max, m) => Math.max(max, m.id), 0) && archivedAt > 0 ? 0 : peerMessages.filter((m) => isHumanInboundMessage(m) && !(m.readBy || []).includes("user") && m.id > cursor).length;
25302
+ const unread = archivedAt >= peerMessages.filter(isHumanInboundMessage).reduce((max, m) => Math.max(max, m.id), 0) && archivedAt > 0 ? 0 : peerMessages.filter((m) => isHumanInboundMessage(m) && !isSessionActivityStep(m) && !(m.readBy || []).includes("user") && m.id > cursor).length;
25059
25303
  const pendingPairInvite = pairs.find((p) => (p.status === "active" || p.status === "pending") && (p.requesterId === agent.id || p.targetId === agent.id))?.status === "pending";
25060
25304
  const claimableTasks = tasks.filter((t) => isClaimableTaskWaiting(t) && targetMatchesAgent(t.target, agent)).length + messages.filter((m) => isClaimableMessageWaiting(m) && targetMatchesAgent(m.to, agent)).length;
25061
25305
  return {
@@ -25154,6 +25398,7 @@ function useAllInboxThreads() {
25154
25398
  peer,
25155
25399
  messages: [],
25156
25400
  lastMessage: null,
25401
+ previewMessage: null,
25157
25402
  attention: {
25158
25403
  unread: 0,
25159
25404
  needsHumanResponse: false,
@@ -25167,8 +25412,17 @@ function useAllInboxThreads() {
25167
25412
  for (const thread of threads.values()) {
25168
25413
  thread.messages.sort((a, b) => a.id - b.id);
25169
25414
  thread.lastMessage = thread.messages[thread.messages.length - 1] || null;
25415
+ let preview = null;
25416
+ for (let i = thread.messages.length - 1; i >= 0; i--) {
25417
+ const m = thread.messages[i];
25418
+ if (m && !isSessionActivityStep(m)) {
25419
+ preview = m;
25420
+ break;
25421
+ }
25422
+ }
25423
+ thread.previewMessage = preview || thread.lastMessage;
25170
25424
  const cursor = Number(readCursors[thread.peer] || 0);
25171
- const unread = thread.messages.filter((m) => isHumanInboundMessage(m) && !(m.readBy || []).includes("user") && m.id > cursor).length;
25425
+ const unread = thread.messages.filter((m) => isHumanInboundMessage(m) && !isSessionActivityStep(m) && !(m.readBy || []).includes("user") && m.id > cursor).length;
25172
25426
  thread.attention = {
25173
25427
  unread,
25174
25428
  needsHumanResponse: false,
@@ -76130,7 +76384,8 @@ var iconMap = {
76130
76384
  FolderTree,
76131
76385
  Shield,
76132
76386
  Wrench,
76133
- UserCog
76387
+ UserCog,
76388
+ Lightbulb
76134
76389
  };
76135
76390
  function notificationTime(notification) {
76136
76391
  return new Date(notification.createdAt).toLocaleTimeString(void 0, {
@@ -125201,7 +125456,7 @@ function AgentListPanel({ threads, onSelectAgent }) {
125201
125456
  }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { children: sortedAgents.map((agent) => {
125202
125457
  const thread = threadByPeer.get(agent.id);
125203
125458
  const unread = thread?.attention.unread || 0;
125204
- const lastMsg = thread?.lastMessage;
125459
+ const lastMsg = thread?.previewMessage;
125205
125460
  const lastActivityAt = threadActivityTimestamp(thread);
125206
125461
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
125207
125462
  className: cn$2("w-full text-left px-3 py-2.5 flex items-start gap-2.5 hover:bg-muted/50 transition-colors border-b border-border/50", selectedInboxThread === agent.id && "bg-muted/60"),
@@ -126342,7 +126597,11 @@ function ChatPanel({ threads, onBack, showBackButton }) {
126342
126597
  const showError = useRelayStore((s) => s.showError);
126343
126598
  const orchestrators = useRelayStore((s) => s.orchestrators);
126344
126599
  const fetchOrchestrators = useRelayStore((s) => s.fetchOrchestrators);
126600
+ const voiceTtsEnabled = useRelayStore((s) => s.voiceTtsEnabled);
126601
+ const setVoiceTtsEnabled = useRelayStore((s) => s.setVoiceTtsEnabled);
126345
126602
  const fileInputRef = (0, import_react.useRef)(null);
126603
+ const pttRecorderRef = (0, import_react.useRef)(null);
126604
+ const [micState, setMicState] = (0, import_react.useState)("idle");
126346
126605
  const pendingAttachmentsRef = (0, import_react.useRef)([]);
126347
126606
  const [pendingAttachments, setPendingAttachments] = (0, import_react.useState)([]);
126348
126607
  const [dragActive, setDragActive] = (0, import_react.useState)(false);
@@ -126633,6 +126892,36 @@ function ChatPanel({ threads, onBack, showBackButton }) {
126633
126892
  e.preventDefault();
126634
126893
  uploadFiles(files);
126635
126894
  }
126895
+ async function startPtt() {
126896
+ if (!micAvailable || micState !== "idle" || !selectedInboxThread) return;
126897
+ voiceTts.bargeIn();
126898
+ const recorder = new PttRecorder();
126899
+ try {
126900
+ await recorder.start();
126901
+ pttRecorderRef.current = recorder;
126902
+ setMicState("recording");
126903
+ } catch (e) {
126904
+ pttRecorderRef.current = null;
126905
+ setMicState("idle");
126906
+ showError("Microphone unavailable", e?.message || "Could not access the microphone.");
126907
+ }
126908
+ }
126909
+ async function stopPtt() {
126910
+ const recorder = pttRecorderRef.current;
126911
+ if (!recorder || micState !== "recording") return;
126912
+ pttRecorderRef.current = null;
126913
+ setMicState("transcribing");
126914
+ try {
126915
+ const clip = await recorder.stop();
126916
+ if (!clip) return;
126917
+ const text = await transcribeClip(clip);
126918
+ if (text) setReplyDraft(selectedInboxThread, draft ? `${draft} ${text}` : text);
126919
+ } catch (e) {
126920
+ showError("Transcription failed", e?.message || "Could not transcribe audio.");
126921
+ } finally {
126922
+ setMicState("idle");
126923
+ }
126924
+ }
126636
126925
  if (!selectedInboxThread) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
126637
126926
  className: "flex flex-col items-center justify-center h-full text-center px-4",
126638
126927
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(MessageSquare, { className: "w-12 h-12 text-zinc-600 mb-3" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
@@ -126699,6 +126988,14 @@ function ChatPanel({ threads, onBack, showBackButton }) {
126699
126988
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
126700
126989
  className: "flex items-center gap-0.5 md:gap-1 shrink-0",
126701
126990
  children: agent && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
126991
+ voiceTts.available && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
126992
+ variant: "ghost",
126993
+ size: "icon-sm",
126994
+ title: voiceTtsEnabled ? "Speaking agent responses aloud — click to mute" : "Speak agent responses aloud (active chat)",
126995
+ className: voiceTtsEnabled ? "text-primary" : "",
126996
+ onClick: () => setVoiceTtsEnabled(!voiceTtsEnabled),
126997
+ children: voiceTtsEnabled ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Volume2, { className: "w-3.5 h-3.5" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(VolumeX, { className: "w-3.5 h-3.5" })
126998
+ }),
126702
126999
  canOpenTerminal && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
126703
127000
  variant: "ghost",
126704
127001
  size: "icon-sm",
@@ -126960,6 +127257,26 @@ function ChatPanel({ threads, onBack, showBackButton }) {
126960
127257
  placeholder: `Message ${agent ? displayName(agent) : selectedInboxThread}…`,
126961
127258
  className: "flex-1"
126962
127259
  }),
127260
+ micAvailable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
127261
+ variant: "ghost",
127262
+ size: "icon",
127263
+ title: micState === "recording" ? "Release to transcribe" : "Hold to talk",
127264
+ disabled: chatSending || micState === "transcribing",
127265
+ onPointerDown: (e) => {
127266
+ e.preventDefault();
127267
+ e.currentTarget.setPointerCapture(e.pointerId);
127268
+ startPtt();
127269
+ },
127270
+ onPointerUp: (e) => {
127271
+ e.preventDefault();
127272
+ try {
127273
+ e.currentTarget.releasePointerCapture(e.pointerId);
127274
+ } catch {}
127275
+ stopPtt();
127276
+ },
127277
+ className: `shrink-0 mb-0.5 rounded-xl h-[42px] w-[42px] ${micState === "recording" ? "text-red-400 animate-pulse" : ""}`,
127278
+ children: micState === "transcribing" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoaderCircle, { className: "w-4 h-4 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Mic, { className: "w-4 h-4" })
127279
+ }),
126963
127280
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
126964
127281
  size: "icon",
126965
127282
  disabled: !draft.trim() && readyAttachments.length === 0 || hasPendingUploads || chatSending,
@@ -126980,14 +127297,36 @@ function ChatPanel({ threads, onBack, showBackButton }) {
126980
127297
  className: "w-full"
126981
127298
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
126982
127299
  className: "flex items-center justify-between",
126983
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
126984
- variant: "ghost",
126985
- size: "icon",
126986
- title: "Attach files",
126987
- disabled: chatSending,
126988
- onClick: () => fileInputRef.current?.click(),
126989
- className: "rounded-xl h-9 w-9",
126990
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Paperclip, { className: "w-4 h-4" })
127300
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
127301
+ className: "flex items-center gap-2",
127302
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
127303
+ variant: "ghost",
127304
+ size: "icon",
127305
+ title: "Attach files",
127306
+ disabled: chatSending,
127307
+ onClick: () => fileInputRef.current?.click(),
127308
+ className: "rounded-xl h-9 w-9",
127309
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Paperclip, { className: "w-4 h-4" })
127310
+ }), micAvailable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
127311
+ variant: "ghost",
127312
+ size: "icon",
127313
+ title: micState === "recording" ? "Release to transcribe" : "Hold to talk",
127314
+ disabled: chatSending || micState === "transcribing",
127315
+ onPointerDown: (e) => {
127316
+ e.preventDefault();
127317
+ e.currentTarget.setPointerCapture(e.pointerId);
127318
+ startPtt();
127319
+ },
127320
+ onPointerUp: (e) => {
127321
+ e.preventDefault();
127322
+ try {
127323
+ e.currentTarget.releasePointerCapture(e.pointerId);
127324
+ } catch {}
127325
+ stopPtt();
127326
+ },
127327
+ className: `rounded-xl h-9 w-9 ${micState === "recording" ? "text-red-400 animate-pulse" : ""}`,
127328
+ children: micState === "transcribing" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoaderCircle, { className: "w-4 h-4 animate-spin" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Mic, { className: "w-4 h-4" })
127329
+ })]
126991
127330
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
126992
127331
  size: "icon",
126993
127332
  disabled: !draft.trim() && readyAttachments.length === 0 || hasPendingUploads || chatSending,
@@ -152306,6 +152645,368 @@ function AnalyticsView() {
152306
152645
  });
152307
152646
  }
152308
152647
  //#endregion
152648
+ //#region src/components/views/insights.tsx
152649
+ var DEFAULT_CONFIG = {
152650
+ enabled: true,
152651
+ contextRatio: { enabled: true },
152652
+ introspection: {
152653
+ enabled: true,
152654
+ minTurns: 4,
152655
+ minContextRemaining: .15
152656
+ }
152657
+ };
152658
+ function compactJson(value) {
152659
+ if (!value || Object.keys(value).length === 0) return "—";
152660
+ return Object.entries(value).map(([k, v]) => `${k}: ${typeof v === "number" ? Math.round(v * 1e3) / 1e3 : String(v)}`).join(" ");
152661
+ }
152662
+ function InsightsView() {
152663
+ const [config, setConfig] = (0, import_react.useState)(DEFAULT_CONFIG);
152664
+ const [version, setVersion] = (0, import_react.useState)(0);
152665
+ const [observations, setObservations] = (0, import_react.useState)([]);
152666
+ const [stats, setStats] = (0, import_react.useState)([]);
152667
+ const [projects, setProjects] = (0, import_react.useState)([]);
152668
+ const [projectFilter, setProjectFilter] = (0, import_react.useState)("");
152669
+ const [signalFilter, setSignalFilter] = (0, import_react.useState)("");
152670
+ const [saving, setSaving] = (0, import_react.useState)(false);
152671
+ const [error, setError] = (0, import_react.useState)(null);
152672
+ const [now, setNow] = (0, import_react.useState)(() => Date.now());
152673
+ async function refresh() {
152674
+ setError(null);
152675
+ try {
152676
+ const params = new URLSearchParams();
152677
+ if (projectFilter) params.set("project", projectFilter);
152678
+ if (signalFilter) params.set("signal", signalFilter);
152679
+ const query = params.toString();
152680
+ const [cfg, obs] = await Promise.all([api("GET", "/insights/config"), api("GET", `/insights/observations${query ? `?${query}` : ""}`)]);
152681
+ setConfig(cfg.value);
152682
+ setVersion(cfg.version);
152683
+ setObservations(obs.observations);
152684
+ setStats(obs.stats);
152685
+ setProjects(obs.projects);
152686
+ setNow(Date.now());
152687
+ } catch (e) {
152688
+ setError(e instanceof Error ? e.message : String(e));
152689
+ }
152690
+ }
152691
+ (0, import_react.useEffect)(() => {
152692
+ refresh();
152693
+ }, [projectFilter, signalFilter]);
152694
+ async function saveConfig(next) {
152695
+ setSaving(true);
152696
+ setError(null);
152697
+ const previous = config;
152698
+ setConfig(next);
152699
+ try {
152700
+ const entry = await api("PUT", "/insights/config", {
152701
+ value: next,
152702
+ updatedBy: "dashboard"
152703
+ });
152704
+ setConfig(entry.value);
152705
+ setVersion(entry.version);
152706
+ } catch (e) {
152707
+ setConfig(previous);
152708
+ setError(e instanceof Error ? e.message : String(e));
152709
+ } finally {
152710
+ setSaving(false);
152711
+ }
152712
+ }
152713
+ const signals = (0, import_react.useMemo)(() => {
152714
+ return [...new Set(stats.map((s) => s.signal))].sort();
152715
+ }, [stats]);
152716
+ const globalStats = (0, import_react.useMemo)(() => stats.filter((s) => s.project === null), [stats]);
152717
+ const projectStats = (0, import_react.useMemo)(() => stats.filter((s) => s.project !== null), [stats]);
152718
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
152719
+ className: "space-y-4 p-4",
152720
+ children: [
152721
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
152722
+ className: "flex items-center justify-between",
152723
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
152724
+ className: "flex items-center gap-2",
152725
+ children: [
152726
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Lightbulb, { className: "size-5 text-primary" }),
152727
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", {
152728
+ className: "text-lg font-semibold",
152729
+ children: "Insights"
152730
+ }),
152731
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
152732
+ className: "text-xs text-muted-foreground",
152733
+ children: "continuous self-improvement · epic #183"
152734
+ })
152735
+ ]
152736
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
152737
+ variant: "outline",
152738
+ size: "sm",
152739
+ onClick: () => void refresh(),
152740
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RefreshCw, { className: "size-4" }), " Refresh"]
152741
+ })]
152742
+ }),
152743
+ error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
152744
+ className: "rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive",
152745
+ children: error
152746
+ }),
152747
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(CardTitle, {
152748
+ className: "flex items-center justify-between",
152749
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "Features" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
152750
+ className: "text-xs font-normal text-muted-foreground",
152751
+ children: [
152752
+ "config v",
152753
+ version,
152754
+ " · saved by ",
152755
+ config && "dashboard"
152756
+ ]
152757
+ })]
152758
+ }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(CardContent, {
152759
+ className: "space-y-4",
152760
+ children: [
152761
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
152762
+ className: "flex items-center justify-between",
152763
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
152764
+ className: "text-sm font-medium",
152765
+ children: "Insights enabled"
152766
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
152767
+ className: "text-xs text-muted-foreground",
152768
+ children: "Master switch. When off, no signals are recorded regardless of the toggles below."
152769
+ })] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Switch, {
152770
+ checked: config.enabled,
152771
+ disabled: saving,
152772
+ onCheckedChange: (v) => void saveConfig({
152773
+ ...config,
152774
+ enabled: v
152775
+ })
152776
+ })]
152777
+ }),
152778
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
152779
+ className: "flex items-center justify-between border-t border-border pt-3",
152780
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
152781
+ className: "text-sm font-medium",
152782
+ children: ["Context-gathering ratio ", /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
152783
+ className: "text-xs text-muted-foreground",
152784
+ children: "(#184)"
152785
+ })]
152786
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
152787
+ className: "text-xs text-muted-foreground",
152788
+ children: "Server-side: read/search vs. action before first substantive move, per session."
152789
+ })] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Switch, {
152790
+ checked: config.contextRatio.enabled,
152791
+ disabled: saving || !config.enabled,
152792
+ onCheckedChange: (v) => void saveConfig({
152793
+ ...config,
152794
+ contextRatio: { enabled: v }
152795
+ })
152796
+ })]
152797
+ }),
152798
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
152799
+ className: "border-t border-border pt-3",
152800
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
152801
+ className: "flex items-center justify-between",
152802
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
152803
+ className: "text-sm font-medium",
152804
+ children: ["End-of-session introspection ", /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
152805
+ className: "text-xs text-muted-foreground",
152806
+ children: "(#185)"
152807
+ })]
152808
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
152809
+ className: "text-xs text-muted-foreground",
152810
+ children: "Agent-authored 3-field artifact at session end. Gated below."
152811
+ })] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Switch, {
152812
+ checked: config.introspection.enabled,
152813
+ disabled: saving || !config.enabled,
152814
+ onCheckedChange: (v) => void saveConfig({
152815
+ ...config,
152816
+ introspection: {
152817
+ ...config.introspection,
152818
+ enabled: v
152819
+ }
152820
+ })
152821
+ })]
152822
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
152823
+ className: "mt-3 flex flex-wrap gap-4",
152824
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", {
152825
+ className: "flex items-center gap-2 text-xs text-muted-foreground",
152826
+ children: ["min turns", /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
152827
+ type: "number",
152828
+ className: "h-7 w-20",
152829
+ value: config.introspection.minTurns,
152830
+ disabled: saving || !config.enabled || !config.introspection.enabled,
152831
+ onChange: (e) => setConfig({
152832
+ ...config,
152833
+ introspection: {
152834
+ ...config.introspection,
152835
+ minTurns: Number(e.target.value)
152836
+ }
152837
+ }),
152838
+ onBlur: () => void saveConfig(config)
152839
+ })]
152840
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", {
152841
+ className: "flex items-center gap-2 text-xs text-muted-foreground",
152842
+ children: ["min context remaining (0–1)", /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
152843
+ type: "number",
152844
+ step: "0.05",
152845
+ min: "0",
152846
+ max: "1",
152847
+ className: "h-7 w-24",
152848
+ value: config.introspection.minContextRemaining,
152849
+ disabled: saving || !config.enabled || !config.introspection.enabled,
152850
+ onChange: (e) => setConfig({
152851
+ ...config,
152852
+ introspection: {
152853
+ ...config.introspection,
152854
+ minContextRemaining: Number(e.target.value)
152855
+ }
152856
+ }),
152857
+ onBlur: () => void saveConfig(config)
152858
+ })]
152859
+ })]
152860
+ })]
152861
+ })
152862
+ ]
152863
+ })] }),
152864
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardTitle, { children: "Signals" }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardContent, { children: globalStats.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
152865
+ className: "text-sm text-muted-foreground",
152866
+ children: "No observations yet — they appear after sessions end with Insights enabled."
152867
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
152868
+ className: "space-y-4",
152869
+ children: globalStats.map((g) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
152870
+ className: "mb-1 flex items-center gap-2",
152871
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge$1, {
152872
+ variant: "secondary",
152873
+ children: g.signal
152874
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
152875
+ className: "text-xs text-muted-foreground",
152876
+ children: [
152877
+ g.count,
152878
+ " obs · ",
152879
+ g.avgRatio !== null ? `avg ratio ${g.avgRatio.toFixed(2)}` : "no ratio",
152880
+ " · ",
152881
+ timeAgo(now, g.lastAt)
152882
+ ]
152883
+ })]
152884
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
152885
+ className: "flex flex-wrap gap-2 pl-2",
152886
+ children: projectStats.filter((p) => p.signal === g.signal).map((p) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
152887
+ className: "rounded border border-border px-2 py-0.5 text-xs text-muted-foreground",
152888
+ children: [
152889
+ p.project,
152890
+ ": ",
152891
+ p.count,
152892
+ p.avgRatio !== null ? ` · ${p.avgRatio.toFixed(2)}` : ""
152893
+ ]
152894
+ }, `${g.signal}-${p.project}`))
152895
+ })] }, g.signal))
152896
+ }) })] }),
152897
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(CardTitle, {
152898
+ className: "flex flex-wrap items-center gap-3",
152899
+ children: [
152900
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "Observations" }),
152901
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
152902
+ className: "h-7 rounded border border-border bg-background px-2 text-xs",
152903
+ value: projectFilter,
152904
+ onChange: (e) => setProjectFilter(e.target.value),
152905
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
152906
+ value: "",
152907
+ children: "all projects"
152908
+ }), projects.map((p) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
152909
+ value: p,
152910
+ children: p
152911
+ }, p))]
152912
+ }),
152913
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
152914
+ className: "h-7 rounded border border-border bg-background px-2 text-xs",
152915
+ value: signalFilter,
152916
+ onChange: (e) => setSignalFilter(e.target.value),
152917
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
152918
+ value: "",
152919
+ children: "all signals"
152920
+ }), signals.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
152921
+ value: s,
152922
+ children: s
152923
+ }, s))]
152924
+ })
152925
+ ]
152926
+ }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardContent, { children: observations.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
152927
+ className: "text-sm text-muted-foreground",
152928
+ children: "No observations match."
152929
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
152930
+ className: "overflow-x-auto",
152931
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", {
152932
+ className: "w-full text-left text-xs",
152933
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", {
152934
+ className: "text-muted-foreground",
152935
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", {
152936
+ className: "border-b border-border",
152937
+ children: [
152938
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
152939
+ className: "py-1 pr-3 font-medium",
152940
+ children: "when"
152941
+ }),
152942
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
152943
+ className: "py-1 pr-3 font-medium",
152944
+ children: "project"
152945
+ }),
152946
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
152947
+ className: "py-1 pr-3 font-medium",
152948
+ children: "signal"
152949
+ }),
152950
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
152951
+ className: "py-1 pr-3 font-medium",
152952
+ children: "src"
152953
+ }),
152954
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
152955
+ className: "py-1 pr-3 font-medium",
152956
+ children: "value"
152957
+ }),
152958
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
152959
+ className: "py-1 pr-3 font-medium",
152960
+ children: "outcome"
152961
+ }),
152962
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
152963
+ className: "py-1 pr-3 font-medium",
152964
+ children: "session"
152965
+ })
152966
+ ]
152967
+ })
152968
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: observations.map((o) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", {
152969
+ className: "border-b border-border/50",
152970
+ children: [
152971
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
152972
+ className: "py-1 pr-3 whitespace-nowrap text-muted-foreground",
152973
+ children: timeAgo(now, o.createdAt)
152974
+ }),
152975
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
152976
+ className: "py-1 pr-3 whitespace-nowrap",
152977
+ children: o.project
152978
+ }),
152979
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
152980
+ className: "py-1 pr-3 whitespace-nowrap",
152981
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge$1, {
152982
+ variant: "outline",
152983
+ children: o.signal
152984
+ })
152985
+ }),
152986
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
152987
+ className: "py-1 pr-3 whitespace-nowrap text-muted-foreground",
152988
+ children: o.source
152989
+ }),
152990
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
152991
+ className: "py-1 pr-3 font-mono",
152992
+ children: compactJson(o.value)
152993
+ }),
152994
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
152995
+ className: "py-1 pr-3 font-mono text-muted-foreground",
152996
+ children: compactJson(o.outcome)
152997
+ }),
152998
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
152999
+ className: "py-1 pr-3 whitespace-nowrap text-muted-foreground",
153000
+ children: o.agentId ?? o.sessionId.slice(0, 12)
153001
+ })
153002
+ ]
153003
+ }, o.id)) })]
153004
+ })
153005
+ }) })] })
153006
+ ]
153007
+ });
153008
+ }
153009
+ //#endregion
152309
153010
  //#region src/components/views/maintenance.tsx
152310
153011
  var STATUS_CLASS = {
152311
153012
  succeeded: "bg-emerald-500/10 text-emerald-400 border-emerald-500/30",
@@ -156885,6 +157586,7 @@ var views = {
156885
157586
  tasks: TasksView,
156886
157587
  automation: AutomationView,
156887
157588
  analytics: AnalyticsView,
157589
+ insights: InsightsView,
156888
157590
  maintenance: MaintenanceView,
156889
157591
  settings: SettingsView
156890
157592
  };
@@ -157903,6 +158605,11 @@ if ("serviceWorker" in navigator) {
157903
158605
  height: calc(var(--spacing) * 4);
157904
158606
  }
157905
158607
 
158608
+ .size-5 {
158609
+ width: calc(var(--spacing) * 5);
158610
+ height: calc(var(--spacing) * 5);
158611
+ }
158612
+
157906
158613
  .size-6 {
157907
158614
  width: calc(var(--spacing) * 6);
157908
158615
  height: calc(var(--spacing) * 6);
@@ -158224,6 +158931,14 @@ if ("serviceWorker" in navigator) {
158224
158931
  width: calc(var(--spacing) * 14);
158225
158932
  }
158226
158933
 
158934
+ .w-20 {
158935
+ width: calc(var(--spacing) * 20);
158936
+ }
158937
+
158938
+ .w-24 {
158939
+ width: calc(var(--spacing) * 24);
158940
+ }
158941
+
158227
158942
  .w-40 {
158228
158943
  width: calc(var(--spacing) * 40);
158229
158944
  }
@@ -160132,6 +160847,10 @@ if ("serviceWorker" in navigator) {
160132
160847
  padding-top: calc(var(--spacing) * 2);
160133
160848
  }
160134
160849
 
160850
+ .pt-3 {
160851
+ padding-top: calc(var(--spacing) * 3);
160852
+ }
160853
+
160135
160854
  .pt-4 {
160136
160855
  padding-top: calc(var(--spacing) * 4);
160137
160856
  }
@@ -160144,6 +160863,10 @@ if ("serviceWorker" in navigator) {
160144
160863
  padding-right: calc(var(--spacing) * 2);
160145
160864
  }
160146
160865
 
160866
+ .pr-3 {
160867
+ padding-right: calc(var(--spacing) * 3);
160868
+ }
160869
+
160147
160870
  .pr-4 {
160148
160871
  padding-right: calc(var(--spacing) * 4);
160149
160872
  }