agent-relay-server 0.12.4 → 0.14.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,35 @@ 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
+ }
10303
+ /** POST JSON and get an audio (or other) Blob back — used for server-side TTS. */
10304
+ async function apiPostJsonForBlob(path, body) {
10305
+ const headers = { "Content-Type": "application/json" };
10306
+ if (authToken) headers["X-Agent-Relay-Token"] = authToken;
10307
+ const response = await fetch(new URL("api" + path, baseUrl()), {
10308
+ method: "POST",
10309
+ headers,
10310
+ body: JSON.stringify(body)
10311
+ });
10312
+ if (!response.ok) {
10313
+ if (response.status === 401) throw makeError(401, "Authentication required");
10314
+ throw makeError(response.status, await responseErrorMessage(response));
10315
+ }
10316
+ return response.blob();
10317
+ }
10289
10318
  async function apiBlob(path) {
10290
10319
  const opts = {
10291
10320
  method: "GET",
@@ -10301,6 +10330,290 @@ async function apiBlob(path) {
10301
10330
  return response.blob();
10302
10331
  }
10303
10332
  //#endregion
10333
+ //#region src/lib/voice.ts
10334
+ /**
10335
+ * Browser voice I/O for chat.
10336
+ *
10337
+ * TTS (output) is 100% browser-native via the Web Speech API — the store
10338
+ * already receives every agent response turn, so there is no backend round-trip.
10339
+ * STT (input) records the mic and posts the clip to the voice connector's
10340
+ * whisper endpoint through the relay proxy (single-origin, auth-gated).
10341
+ *
10342
+ * Speaker policy ("active chat owns the speaker"):
10343
+ * - One utterance at a time. Disabled => silent.
10344
+ * - Only the active chat speaks, and only responses that arrive while it is
10345
+ * active (no backlog replay).
10346
+ * - Switching away lets the current utterance finish but drops the now-background
10347
+ * chat's queued + future responses.
10348
+ * - The active chat preempts: if it speaks while a previous chat's audio lingers,
10349
+ * that audio is cancelled.
10350
+ */
10351
+ /** Collapse markdown/code into something worth hearing (mirrors the connector's text.ts). */
10352
+ function speechify(markdown) {
10353
+ if (!markdown) return "";
10354
+ let text = markdown.replace(/\r\n/g, "\n");
10355
+ text = text.replace(/```[^\n]*\n([\s\S]*?)```/g, (_m, body) => {
10356
+ const lines = body.replace(/\n+$/, "").split("\n").length;
10357
+ return ` code block, ${lines} ${lines === 1 ? "line" : "lines"}. `;
10358
+ });
10359
+ text = text.replace(/```[^\n]*/g, " code block. ");
10360
+ text = text.replace(/^#{1,6}\s+/gm, "");
10361
+ text = text.replace(/^\s*[-*+]\s+/gm, ". ");
10362
+ text = text.replace(/^\s*\d+\.\s+/gm, ". ");
10363
+ text = text.replace(/`([^`]+)`/g, "$1");
10364
+ text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
10365
+ text = text.replace(/(\*\*|__|\*|_)(.*?)\1/g, "$2");
10366
+ text = text.replace(/[ \t]+/g, " ");
10367
+ text = text.replace(/\n{2,}/g, ". ");
10368
+ text = text.replace(/\s*\.\s*\.\s*(\.\s*)+/g, ". ");
10369
+ return text.trim();
10370
+ }
10371
+ var MAX_CHUNK = 220;
10372
+ /** Split into utterance-sized chunks (sentence boundaries; hard-split very long runs). */
10373
+ function chunkForSpeech(text) {
10374
+ const sentences = text.match(/[^.!?]+[.!?]*\s*/g)?.map((s) => s.trim()).filter(Boolean) ?? [text];
10375
+ const out = [];
10376
+ for (const s of sentences) {
10377
+ if (s.length <= MAX_CHUNK) {
10378
+ out.push(s);
10379
+ continue;
10380
+ }
10381
+ let rest = s;
10382
+ while (rest.length > MAX_CHUNK) {
10383
+ let cut = rest.lastIndexOf(" ", MAX_CHUNK);
10384
+ if (cut <= 0) cut = MAX_CHUNK;
10385
+ out.push(rest.slice(0, cut).trim());
10386
+ rest = rest.slice(cut).trim();
10387
+ }
10388
+ if (rest) out.push(rest);
10389
+ }
10390
+ return out;
10391
+ }
10392
+ var synthAvailable = typeof window !== "undefined" && "speechSynthesis" in window;
10393
+ var VoiceTts = class {
10394
+ enabled = false;
10395
+ lang = "en-US";
10396
+ mode = "kokoro";
10397
+ kokoroVoice = "am_michael";
10398
+ active = null;
10399
+ queue = [];
10400
+ currentChat = null;
10401
+ speaking = false;
10402
+ gen = 0;
10403
+ audioEl = null;
10404
+ audioUrl = null;
10405
+ get available() {
10406
+ return synthAvailable || typeof Audio !== "undefined";
10407
+ }
10408
+ isEnabled() {
10409
+ return this.enabled;
10410
+ }
10411
+ setEnabled(on) {
10412
+ if (on === this.enabled) return;
10413
+ this.enabled = on;
10414
+ if (!on) this.reset();
10415
+ }
10416
+ /** Set the spoken-voice language (BCP-47, e.g. "en-US"). Empty = browser locale. */
10417
+ setLang(lang) {
10418
+ this.lang = lang;
10419
+ }
10420
+ setMode(mode) {
10421
+ if (mode === this.mode) return;
10422
+ this.mode = mode;
10423
+ this.reset();
10424
+ }
10425
+ setKokoroVoice(voice) {
10426
+ this.kokoroVoice = voice;
10427
+ }
10428
+ setActiveChat(chatId) {
10429
+ if (chatId === this.active) return;
10430
+ this.active = chatId;
10431
+ this.queue = this.queue.filter((q) => q.chatId === chatId);
10432
+ }
10433
+ /** A captured agent response turn arrived for `chatId`. */
10434
+ onResponse(chatId, rawText) {
10435
+ if (!this.enabled || !this.available || !chatId || chatId !== this.active) return;
10436
+ const text = speechify(rawText);
10437
+ if (!text) return;
10438
+ if (this.speaking && this.currentChat && this.currentChat !== chatId) {
10439
+ this.queue = [];
10440
+ this.cancel();
10441
+ }
10442
+ this.queue.push({
10443
+ chatId,
10444
+ text
10445
+ });
10446
+ this.pump();
10447
+ }
10448
+ /** Cut all speech immediately (e.g. when the user starts talking). */
10449
+ bargeIn() {
10450
+ this.reset();
10451
+ }
10452
+ reset() {
10453
+ this.queue = [];
10454
+ this.cancel();
10455
+ }
10456
+ cancel() {
10457
+ this.gen++;
10458
+ this.currentChat = null;
10459
+ this.speaking = false;
10460
+ try {
10461
+ window.speechSynthesis.cancel();
10462
+ } catch {}
10463
+ this.stopAudio();
10464
+ }
10465
+ stopAudio() {
10466
+ if (this.audioEl) try {
10467
+ this.audioEl.pause();
10468
+ this.audioEl.src = "";
10469
+ } catch {}
10470
+ if (this.audioUrl) {
10471
+ try {
10472
+ URL.revokeObjectURL(this.audioUrl);
10473
+ } catch {}
10474
+ this.audioUrl = null;
10475
+ }
10476
+ }
10477
+ pump() {
10478
+ if (this.speaking) return;
10479
+ const item = this.queue.shift();
10480
+ if (!item) return;
10481
+ this.speaking = true;
10482
+ this.currentChat = item.chatId;
10483
+ const gen = ++this.gen;
10484
+ const chunks = chunkForSpeech(item.text);
10485
+ const done = () => {
10486
+ if (gen !== this.gen) return;
10487
+ this.speaking = false;
10488
+ this.currentChat = null;
10489
+ this.pump();
10490
+ };
10491
+ if (this.mode === "kokoro") this.speakKokoro(chunks, 0, gen, done);
10492
+ else this.speakBrowser(chunks, 0, gen, done);
10493
+ }
10494
+ speakBrowser(chunks, i, gen, done) {
10495
+ if (gen !== this.gen) return;
10496
+ if (!synthAvailable || i >= chunks.length) return done();
10497
+ const u = new SpeechSynthesisUtterance(chunks[i]);
10498
+ u.lang = this.lang || navigator.language || "en-US";
10499
+ u.onend = () => this.speakBrowser(chunks, i + 1, gen, done);
10500
+ u.onerror = () => this.speakBrowser(chunks, i + 1, gen, done);
10501
+ window.speechSynthesis.speak(u);
10502
+ }
10503
+ speakKokoro(chunks, i, gen, done, prefetched) {
10504
+ if (gen !== this.gen) return;
10505
+ const text = chunks[i];
10506
+ if (text === void 0) return done();
10507
+ const cur = prefetched ?? this.fetchSpeech(text);
10508
+ const nextText = chunks[i + 1];
10509
+ const next = nextText !== void 0 ? this.fetchSpeech(nextText) : void 0;
10510
+ cur.then((blob) => {
10511
+ if (gen !== this.gen) return;
10512
+ this.playBlob(blob, gen, () => this.speakKokoro(chunks, i + 1, gen, done, next), () => {
10513
+ this.speakBrowser(chunks, i, gen, done);
10514
+ });
10515
+ }).catch(() => {
10516
+ if (gen !== this.gen) return;
10517
+ this.speakBrowser(chunks, i, gen, done);
10518
+ });
10519
+ }
10520
+ fetchSpeech(text) {
10521
+ return apiPostJsonForBlob("/connectors/voice/call/speak", {
10522
+ text,
10523
+ voice: this.kokoroVoice
10524
+ });
10525
+ }
10526
+ playBlob(blob, gen, onend, onerror) {
10527
+ if (gen !== this.gen) return;
10528
+ if (typeof Audio === "undefined") return onerror();
10529
+ this.stopAudio();
10530
+ if (!this.audioEl) this.audioEl = new Audio();
10531
+ const url = URL.createObjectURL(blob);
10532
+ this.audioUrl = url;
10533
+ const el = this.audioEl;
10534
+ el.src = url;
10535
+ el.onended = () => {
10536
+ if (gen === this.gen) onend();
10537
+ };
10538
+ el.onerror = () => {
10539
+ if (gen === this.gen) onerror();
10540
+ };
10541
+ el.play().catch(() => {
10542
+ if (gen === this.gen) onerror();
10543
+ });
10544
+ }
10545
+ };
10546
+ var voiceTts = new VoiceTts();
10547
+ /** Sorted unique BCP-47 languages the browser's speech engine can speak. May be empty
10548
+ * until the engine finishes loading voices (listen for `voiceschanged` and re-read). */
10549
+ function availableSpeechLangs() {
10550
+ if (!synthAvailable) return [];
10551
+ try {
10552
+ return [...new Set(window.speechSynthesis.getVoices().map((v) => v.lang).filter(Boolean))].sort();
10553
+ } catch {
10554
+ return [];
10555
+ }
10556
+ }
10557
+ var micAvailable = typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia && typeof window !== "undefined" && "MediaRecorder" in window;
10558
+ function pickMimeType() {
10559
+ for (const m of [
10560
+ "audio/webm;codecs=opus",
10561
+ "audio/webm",
10562
+ "audio/ogg;codecs=opus",
10563
+ "audio/mp4"
10564
+ ]) if (typeof MediaRecorder !== "undefined" && MediaRecorder.isTypeSupported?.(m)) return m;
10565
+ }
10566
+ /** Hold-to-talk mic recorder. start() on press, stop() on release returns the clip. */
10567
+ var PttRecorder = class {
10568
+ rec = null;
10569
+ chunks = [];
10570
+ stream = null;
10571
+ async start() {
10572
+ this.stream = await navigator.mediaDevices.getUserMedia({ audio: true });
10573
+ this.chunks = [];
10574
+ const mime = pickMimeType();
10575
+ this.rec = new MediaRecorder(this.stream, mime ? { mimeType: mime } : void 0);
10576
+ this.rec.ondataavailable = (e) => {
10577
+ if (e.data.size) this.chunks.push(e.data);
10578
+ };
10579
+ this.rec.start();
10580
+ }
10581
+ async stop() {
10582
+ const rec = this.rec;
10583
+ if (!rec) {
10584
+ this.teardown();
10585
+ return null;
10586
+ }
10587
+ const stopped = new Promise((resolve) => {
10588
+ rec.onstop = () => resolve();
10589
+ });
10590
+ try {
10591
+ rec.stop();
10592
+ } catch {}
10593
+ await stopped;
10594
+ const type = rec.mimeType || "audio/webm";
10595
+ const blob = new Blob(this.chunks, { type });
10596
+ this.teardown();
10597
+ return blob.size ? blob : null;
10598
+ }
10599
+ cancel() {
10600
+ try {
10601
+ this.rec?.stop();
10602
+ } catch {}
10603
+ this.teardown();
10604
+ }
10605
+ teardown() {
10606
+ this.stream?.getTracks().forEach((t) => t.stop());
10607
+ this.rec = null;
10608
+ this.stream = null;
10609
+ this.chunks = [];
10610
+ }
10611
+ };
10612
+ /** Send a recorded clip through the relay proxy to the voice connector's whisper STT. */
10613
+ async function transcribeClip(blob) {
10614
+ return ((await apiPostAudio("/connectors/voice/call/transcribe", blob))?.text ?? "").trim();
10615
+ }
10616
+ //#endregion
10304
10617
  //#region src/store/clock.ts
10305
10618
  var useClock = create$1((set, get) => ({
10306
10619
  now: Date.now(),
@@ -10474,6 +10787,11 @@ var NAV_ITEMS = [
10474
10787
  label: "Analytics",
10475
10788
  icon: "AreaChart"
10476
10789
  },
10790
+ {
10791
+ key: "insights",
10792
+ label: "Insights",
10793
+ icon: "Lightbulb"
10794
+ },
10477
10795
  {
10478
10796
  key: "maintenance",
10479
10797
  label: "Maintenance",
@@ -10701,6 +11019,11 @@ function inboxPeer(msg) {
10701
11019
  function isHumanInboundMessage(msg) {
10702
11020
  return msg.to === "user" && msg.from !== "user";
10703
11021
  }
11022
+ function isSessionActivityStep(msg) {
11023
+ if (msg.kind !== "session") return false;
11024
+ const type = (msg.payload?.session)?.type;
11025
+ return type === "reasoning" || type === "tool";
11026
+ }
10704
11027
  function isClaimableTaskWaiting(task) {
10705
11028
  return WAITING_TASK_STATUSES.has(task.status) && !task.claimedBy;
10706
11029
  }
@@ -11636,6 +11959,10 @@ var useRelayStore = create$1()(persist((set, get) => ({
11636
11959
  showOffline: false,
11637
11960
  showBuiltIns: false,
11638
11961
  autoRefresh: true,
11962
+ voiceTtsEnabled: false,
11963
+ voiceTtsLang: "en-US",
11964
+ voiceTtsMode: "kokoro",
11965
+ voiceTtsKokoroVoice: "am_michael",
11639
11966
  agentSort: "status",
11640
11967
  agentSortDir: "asc",
11641
11968
  agentPresetFilter: "",
@@ -11823,6 +12150,22 @@ var useRelayStore = create$1()(persist((set, get) => ({
11823
12150
  _refreshTimer: null,
11824
12151
  _refreshInFlight: false,
11825
12152
  set: (partial) => set(partial),
12153
+ setVoiceTtsEnabled(on) {
12154
+ voiceTts.setEnabled(on);
12155
+ set({ voiceTtsEnabled: on });
12156
+ },
12157
+ setVoiceTtsLang(lang) {
12158
+ voiceTts.setLang(lang);
12159
+ set({ voiceTtsLang: lang });
12160
+ },
12161
+ setVoiceTtsMode(mode) {
12162
+ voiceTts.setMode(mode);
12163
+ set({ voiceTtsMode: mode });
12164
+ },
12165
+ setVoiceTtsKokoroVoice(voice) {
12166
+ voiceTts.setKokoroVoice(voice);
12167
+ set({ voiceTtsKokoroVoice: voice });
12168
+ },
11826
12169
  async init() {
11827
12170
  if (!useRelayStore.persist.hasHydrated()) await new Promise((resolve) => {
11828
12171
  const unsub = useRelayStore.persist.onFinishHydration(() => {
@@ -11832,6 +12175,11 @@ var useRelayStore = create$1()(persist((set, get) => ({
11832
12175
  });
11833
12176
  const token = get().authToken;
11834
12177
  if (token) setAuthToken(token);
12178
+ voiceTts.setEnabled(get().voiceTtsEnabled);
12179
+ voiceTts.setLang(get().voiceTtsLang);
12180
+ voiceTts.setMode(get().voiceTtsMode);
12181
+ voiceTts.setKokoroVoice(get().voiceTtsKokoroVoice);
12182
+ syncVoiceActiveChat(get());
11835
12183
  setUnauthorizedHandler(() => {
11836
12184
  if (!get().authNeeded) set({
11837
12185
  authNeeded: true,
@@ -12345,6 +12693,10 @@ var useRelayStore = create$1()(persist((set, get) => ({
12345
12693
  const msgs = [...s.messages, msg];
12346
12694
  if (msgs.length > 500) msgs.splice(0, msgs.length - 500);
12347
12695
  set({ messages: msgs });
12696
+ if (msg.kind === "session" && msg.from !== "user") {
12697
+ const sess = msg.payload?.session;
12698
+ if (sess?.type === "response" && sess?.origin === "provider") voiceTts.onResponse(inboxPeer(msg), msg.body);
12699
+ }
12348
12700
  const peer = inboxPeer(msg);
12349
12701
  if (isHumanInboundMessage(msg) && peer && s.view === "chat" && s.selectedInboxThread === peer && !isDashboardHidden()) get().markInboxThreadReadTo(peer, msg.id);
12350
12702
  return;
@@ -13914,6 +14266,10 @@ var useRelayStore = create$1()(persist((set, get) => ({
13914
14266
  showOffline: state.showOffline,
13915
14267
  showBuiltIns: state.showBuiltIns,
13916
14268
  autoRefresh: state.autoRefresh,
14269
+ voiceTtsEnabled: state.voiceTtsEnabled,
14270
+ voiceTtsLang: state.voiceTtsLang,
14271
+ voiceTtsMode: state.voiceTtsMode,
14272
+ voiceTtsKokoroVoice: state.voiceTtsKokoroVoice,
13917
14273
  agentSort: state.agentSort,
13918
14274
  agentSortDir: state.agentSortDir,
13919
14275
  agentPresetFilter: state.agentPresetFilter,
@@ -13953,6 +14309,14 @@ var useRelayStore = create$1()(persist((set, get) => ({
13953
14309
  spawnWorkspaceMode: state.spawnWorkspaceMode
13954
14310
  })
13955
14311
  }));
14312
+ var _voiceActiveChat = null;
14313
+ function syncVoiceActiveChat(s) {
14314
+ const active = s.voiceTtsEnabled && s.view === "chat" ? s.selectedInboxThread || null : null;
14315
+ if (active === _voiceActiveChat) return;
14316
+ _voiceActiveChat = active;
14317
+ voiceTts.setActiveChat(active);
14318
+ }
14319
+ useRelayStore.subscribe(syncVoiceActiveChat);
13956
14320
  //#endregion
13957
14321
  //#region src/lib/themes.ts
13958
14322
  var THEMES = [
@@ -25055,7 +25419,7 @@ function useAgentAttention(agent) {
25055
25419
  const peerMessages = messages.filter((m) => inboxPeer(m) === agent.id && !isReactionEventMessage(m));
25056
25420
  const cursor = Number(readCursors[agent.id] || 0);
25057
25421
  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;
25422
+ 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
25423
  const pendingPairInvite = pairs.find((p) => (p.status === "active" || p.status === "pending") && (p.requesterId === agent.id || p.targetId === agent.id))?.status === "pending";
25060
25424
  const claimableTasks = tasks.filter((t) => isClaimableTaskWaiting(t) && targetMatchesAgent(t.target, agent)).length + messages.filter((m) => isClaimableMessageWaiting(m) && targetMatchesAgent(m.to, agent)).length;
25061
25425
  return {
@@ -25154,6 +25518,7 @@ function useAllInboxThreads() {
25154
25518
  peer,
25155
25519
  messages: [],
25156
25520
  lastMessage: null,
25521
+ previewMessage: null,
25157
25522
  attention: {
25158
25523
  unread: 0,
25159
25524
  needsHumanResponse: false,
@@ -25167,8 +25532,17 @@ function useAllInboxThreads() {
25167
25532
  for (const thread of threads.values()) {
25168
25533
  thread.messages.sort((a, b) => a.id - b.id);
25169
25534
  thread.lastMessage = thread.messages[thread.messages.length - 1] || null;
25535
+ let preview = null;
25536
+ for (let i = thread.messages.length - 1; i >= 0; i--) {
25537
+ const m = thread.messages[i];
25538
+ if (m && !isSessionActivityStep(m)) {
25539
+ preview = m;
25540
+ break;
25541
+ }
25542
+ }
25543
+ thread.previewMessage = preview || thread.lastMessage;
25170
25544
  const cursor = Number(readCursors[thread.peer] || 0);
25171
- const unread = thread.messages.filter((m) => isHumanInboundMessage(m) && !(m.readBy || []).includes("user") && m.id > cursor).length;
25545
+ const unread = thread.messages.filter((m) => isHumanInboundMessage(m) && !isSessionActivityStep(m) && !(m.readBy || []).includes("user") && m.id > cursor).length;
25172
25546
  thread.attention = {
25173
25547
  unread,
25174
25548
  needsHumanResponse: false,
@@ -76130,7 +76504,8 @@ var iconMap = {
76130
76504
  FolderTree,
76131
76505
  Shield,
76132
76506
  Wrench,
76133
- UserCog
76507
+ UserCog,
76508
+ Lightbulb
76134
76509
  };
76135
76510
  function notificationTime(notification) {
76136
76511
  return new Date(notification.createdAt).toLocaleTimeString(void 0, {
@@ -125201,7 +125576,7 @@ function AgentListPanel({ threads, onSelectAgent }) {
125201
125576
  }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { children: sortedAgents.map((agent) => {
125202
125577
  const thread = threadByPeer.get(agent.id);
125203
125578
  const unread = thread?.attention.unread || 0;
125204
- const lastMsg = thread?.lastMessage;
125579
+ const lastMsg = thread?.previewMessage;
125205
125580
  const lastActivityAt = threadActivityTimestamp(thread);
125206
125581
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
125207
125582
  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"),
@@ -125309,6 +125684,40 @@ var TIMELINE_STATUS_LABELS = {
125309
125684
  var TIMELINE_STATUSES = new Set(Object.keys(TIMELINE_STATUS_LABELS));
125310
125685
  var STATUS_DEDUPE_WINDOW_MS = 3e3;
125311
125686
  var CHAT_BOTTOM_THRESHOLD_PX = 96;
125687
+ var KOKORO_VOICES = [
125688
+ {
125689
+ id: "am_michael",
125690
+ label: "Michael (US ♂)"
125691
+ },
125692
+ {
125693
+ id: "am_adam",
125694
+ label: "Adam (US ♂)"
125695
+ },
125696
+ {
125697
+ id: "af_heart",
125698
+ label: "Heart (US ♀)"
125699
+ },
125700
+ {
125701
+ id: "af_bella",
125702
+ label: "Bella (US ♀)"
125703
+ },
125704
+ {
125705
+ id: "af_nicole",
125706
+ label: "Nicole (US ♀)"
125707
+ },
125708
+ {
125709
+ id: "af_sarah",
125710
+ label: "Sarah (US ♀)"
125711
+ },
125712
+ {
125713
+ id: "bm_george",
125714
+ label: "George (UK ♂)"
125715
+ },
125716
+ {
125717
+ id: "bf_emma",
125718
+ label: "Emma (UK ♀)"
125719
+ }
125720
+ ];
125312
125721
  function StatusMarker({ event }) {
125313
125722
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125314
125723
  className: "flex items-center justify-center gap-2 py-2 my-1",
@@ -126342,7 +126751,25 @@ function ChatPanel({ threads, onBack, showBackButton }) {
126342
126751
  const showError = useRelayStore((s) => s.showError);
126343
126752
  const orchestrators = useRelayStore((s) => s.orchestrators);
126344
126753
  const fetchOrchestrators = useRelayStore((s) => s.fetchOrchestrators);
126754
+ const voiceTtsEnabled = useRelayStore((s) => s.voiceTtsEnabled);
126755
+ const setVoiceTtsEnabled = useRelayStore((s) => s.setVoiceTtsEnabled);
126756
+ const voiceTtsLang = useRelayStore((s) => s.voiceTtsLang);
126757
+ const setVoiceTtsLang = useRelayStore((s) => s.setVoiceTtsLang);
126758
+ const voiceTtsMode = useRelayStore((s) => s.voiceTtsMode);
126759
+ const setVoiceTtsMode = useRelayStore((s) => s.setVoiceTtsMode);
126760
+ const voiceTtsKokoroVoice = useRelayStore((s) => s.voiceTtsKokoroVoice);
126761
+ const setVoiceTtsKokoroVoice = useRelayStore((s) => s.setVoiceTtsKokoroVoice);
126762
+ const [speechLangs, setSpeechLangs] = (0, import_react.useState)(() => availableSpeechLangs());
126763
+ (0, import_react.useEffect)(() => {
126764
+ if (!voiceTts.available) return;
126765
+ const refresh = () => setSpeechLangs(availableSpeechLangs());
126766
+ refresh();
126767
+ window.speechSynthesis.addEventListener?.("voiceschanged", refresh);
126768
+ return () => window.speechSynthesis.removeEventListener?.("voiceschanged", refresh);
126769
+ }, []);
126345
126770
  const fileInputRef = (0, import_react.useRef)(null);
126771
+ const pttRecorderRef = (0, import_react.useRef)(null);
126772
+ const [micState, setMicState] = (0, import_react.useState)("idle");
126346
126773
  const pendingAttachmentsRef = (0, import_react.useRef)([]);
126347
126774
  const [pendingAttachments, setPendingAttachments] = (0, import_react.useState)([]);
126348
126775
  const [dragActive, setDragActive] = (0, import_react.useState)(false);
@@ -126633,6 +127060,36 @@ function ChatPanel({ threads, onBack, showBackButton }) {
126633
127060
  e.preventDefault();
126634
127061
  uploadFiles(files);
126635
127062
  }
127063
+ async function startPtt() {
127064
+ if (!micAvailable || micState !== "idle" || !selectedInboxThread) return;
127065
+ voiceTts.bargeIn();
127066
+ const recorder = new PttRecorder();
127067
+ try {
127068
+ await recorder.start();
127069
+ pttRecorderRef.current = recorder;
127070
+ setMicState("recording");
127071
+ } catch (e) {
127072
+ pttRecorderRef.current = null;
127073
+ setMicState("idle");
127074
+ showError("Microphone unavailable", e?.message || "Could not access the microphone.");
127075
+ }
127076
+ }
127077
+ async function stopPtt() {
127078
+ const recorder = pttRecorderRef.current;
127079
+ if (!recorder || micState !== "recording") return;
127080
+ pttRecorderRef.current = null;
127081
+ setMicState("transcribing");
127082
+ try {
127083
+ const clip = await recorder.stop();
127084
+ if (!clip) return;
127085
+ const text = await transcribeClip(clip);
127086
+ if (text) setReplyDraft(selectedInboxThread, draft ? `${draft} ${text}` : text);
127087
+ } catch (e) {
127088
+ showError("Transcription failed", e?.message || "Could not transcribe audio.");
127089
+ } finally {
127090
+ setMicState("idle");
127091
+ }
127092
+ }
126636
127093
  if (!selectedInboxThread) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
126637
127094
  className: "flex flex-col items-center justify-center h-full text-center px-4",
126638
127095
  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 +127156,55 @@ function ChatPanel({ threads, onBack, showBackButton }) {
126699
127156
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
126700
127157
  className: "flex items-center gap-0.5 md:gap-1 shrink-0",
126701
127158
  children: agent && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
127159
+ voiceTts.available && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
127160
+ variant: "ghost",
127161
+ size: "icon-sm",
127162
+ title: voiceTtsEnabled ? "Speaking agent responses aloud — click to mute" : "Speak agent responses aloud (active chat)",
127163
+ className: voiceTtsEnabled ? "text-primary" : "",
127164
+ onClick: () => setVoiceTtsEnabled(!voiceTtsEnabled),
127165
+ 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" })
127166
+ }),
127167
+ voiceTts.available && voiceTtsEnabled && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
127168
+ value: voiceTtsMode,
127169
+ onChange: (e) => setVoiceTtsMode(e.target.value),
127170
+ title: "Voice engine — Kokoro (server, natural) falls back to browser automatically",
127171
+ className: "h-7 rounded border border-border bg-background px-1 text-xs",
127172
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
127173
+ value: "kokoro",
127174
+ children: "Kokoro"
127175
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
127176
+ value: "browser",
127177
+ children: "Browser"
127178
+ })]
127179
+ }), voiceTtsMode === "kokoro" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("select", {
127180
+ value: voiceTtsKokoroVoice,
127181
+ onChange: (e) => setVoiceTtsKokoroVoice(e.target.value),
127182
+ title: "Kokoro voice",
127183
+ className: "h-7 rounded border border-border bg-background px-1 text-xs",
127184
+ children: [...KOKORO_VOICES, ...KOKORO_VOICES.some((v) => v.id === voiceTtsKokoroVoice) ? [] : [{
127185
+ id: voiceTtsKokoroVoice,
127186
+ label: voiceTtsKokoroVoice
127187
+ }]].map((v) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
127188
+ value: v.id,
127189
+ children: v.label
127190
+ }, v.id))
127191
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
127192
+ value: voiceTtsLang,
127193
+ onChange: (e) => setVoiceTtsLang(e.target.value),
127194
+ title: "Voice language",
127195
+ className: "h-7 rounded border border-border bg-background px-1 text-xs",
127196
+ children: [[...new Set([
127197
+ "en-US",
127198
+ ...speechLangs,
127199
+ ...voiceTtsLang ? [voiceTtsLang] : []
127200
+ ])].sort().map((l) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
127201
+ value: l,
127202
+ children: l
127203
+ }, l)), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
127204
+ value: "",
127205
+ children: "Browser default"
127206
+ })]
127207
+ })] }),
126702
127208
  canOpenTerminal && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
126703
127209
  variant: "ghost",
126704
127210
  size: "icon-sm",
@@ -126960,6 +127466,26 @@ function ChatPanel({ threads, onBack, showBackButton }) {
126960
127466
  placeholder: `Message ${agent ? displayName(agent) : selectedInboxThread}…`,
126961
127467
  className: "flex-1"
126962
127468
  }),
127469
+ micAvailable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
127470
+ variant: "ghost",
127471
+ size: "icon",
127472
+ title: micState === "recording" ? "Release to transcribe" : "Hold to talk",
127473
+ disabled: chatSending || micState === "transcribing",
127474
+ onPointerDown: (e) => {
127475
+ e.preventDefault();
127476
+ e.currentTarget.setPointerCapture(e.pointerId);
127477
+ startPtt();
127478
+ },
127479
+ onPointerUp: (e) => {
127480
+ e.preventDefault();
127481
+ try {
127482
+ e.currentTarget.releasePointerCapture(e.pointerId);
127483
+ } catch {}
127484
+ stopPtt();
127485
+ },
127486
+ className: `shrink-0 mb-0.5 rounded-xl h-[42px] w-[42px] ${micState === "recording" ? "text-red-400 animate-pulse" : ""}`,
127487
+ 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" })
127488
+ }),
126963
127489
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
126964
127490
  size: "icon",
126965
127491
  disabled: !draft.trim() && readyAttachments.length === 0 || hasPendingUploads || chatSending,
@@ -126980,14 +127506,36 @@ function ChatPanel({ threads, onBack, showBackButton }) {
126980
127506
  className: "w-full"
126981
127507
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
126982
127508
  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" })
127509
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
127510
+ className: "flex items-center gap-2",
127511
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
127512
+ variant: "ghost",
127513
+ size: "icon",
127514
+ title: "Attach files",
127515
+ disabled: chatSending,
127516
+ onClick: () => fileInputRef.current?.click(),
127517
+ className: "rounded-xl h-9 w-9",
127518
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Paperclip, { className: "w-4 h-4" })
127519
+ }), micAvailable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
127520
+ variant: "ghost",
127521
+ size: "icon",
127522
+ title: micState === "recording" ? "Release to transcribe" : "Hold to talk",
127523
+ disabled: chatSending || micState === "transcribing",
127524
+ onPointerDown: (e) => {
127525
+ e.preventDefault();
127526
+ e.currentTarget.setPointerCapture(e.pointerId);
127527
+ startPtt();
127528
+ },
127529
+ onPointerUp: (e) => {
127530
+ e.preventDefault();
127531
+ try {
127532
+ e.currentTarget.releasePointerCapture(e.pointerId);
127533
+ } catch {}
127534
+ stopPtt();
127535
+ },
127536
+ className: `rounded-xl h-9 w-9 ${micState === "recording" ? "text-red-400 animate-pulse" : ""}`,
127537
+ 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" })
127538
+ })]
126991
127539
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
126992
127540
  size: "icon",
126993
127541
  disabled: !draft.trim() && readyAttachments.length === 0 || hasPendingUploads || chatSending,
@@ -152306,6 +152854,368 @@ function AnalyticsView() {
152306
152854
  });
152307
152855
  }
152308
152856
  //#endregion
152857
+ //#region src/components/views/insights.tsx
152858
+ var DEFAULT_CONFIG = {
152859
+ enabled: true,
152860
+ contextRatio: { enabled: true },
152861
+ introspection: {
152862
+ enabled: true,
152863
+ minTurns: 4,
152864
+ minContextRemaining: .15
152865
+ }
152866
+ };
152867
+ function compactJson(value) {
152868
+ if (!value || Object.keys(value).length === 0) return "—";
152869
+ return Object.entries(value).map(([k, v]) => `${k}: ${typeof v === "number" ? Math.round(v * 1e3) / 1e3 : String(v)}`).join(" ");
152870
+ }
152871
+ function InsightsView() {
152872
+ const [config, setConfig] = (0, import_react.useState)(DEFAULT_CONFIG);
152873
+ const [version, setVersion] = (0, import_react.useState)(0);
152874
+ const [observations, setObservations] = (0, import_react.useState)([]);
152875
+ const [stats, setStats] = (0, import_react.useState)([]);
152876
+ const [projects, setProjects] = (0, import_react.useState)([]);
152877
+ const [projectFilter, setProjectFilter] = (0, import_react.useState)("");
152878
+ const [signalFilter, setSignalFilter] = (0, import_react.useState)("");
152879
+ const [saving, setSaving] = (0, import_react.useState)(false);
152880
+ const [error, setError] = (0, import_react.useState)(null);
152881
+ const [now, setNow] = (0, import_react.useState)(() => Date.now());
152882
+ async function refresh() {
152883
+ setError(null);
152884
+ try {
152885
+ const params = new URLSearchParams();
152886
+ if (projectFilter) params.set("project", projectFilter);
152887
+ if (signalFilter) params.set("signal", signalFilter);
152888
+ const query = params.toString();
152889
+ const [cfg, obs] = await Promise.all([api("GET", "/insights/config"), api("GET", `/insights/observations${query ? `?${query}` : ""}`)]);
152890
+ setConfig(cfg.value);
152891
+ setVersion(cfg.version);
152892
+ setObservations(obs.observations);
152893
+ setStats(obs.stats);
152894
+ setProjects(obs.projects);
152895
+ setNow(Date.now());
152896
+ } catch (e) {
152897
+ setError(e instanceof Error ? e.message : String(e));
152898
+ }
152899
+ }
152900
+ (0, import_react.useEffect)(() => {
152901
+ refresh();
152902
+ }, [projectFilter, signalFilter]);
152903
+ async function saveConfig(next) {
152904
+ setSaving(true);
152905
+ setError(null);
152906
+ const previous = config;
152907
+ setConfig(next);
152908
+ try {
152909
+ const entry = await api("PUT", "/insights/config", {
152910
+ value: next,
152911
+ updatedBy: "dashboard"
152912
+ });
152913
+ setConfig(entry.value);
152914
+ setVersion(entry.version);
152915
+ } catch (e) {
152916
+ setConfig(previous);
152917
+ setError(e instanceof Error ? e.message : String(e));
152918
+ } finally {
152919
+ setSaving(false);
152920
+ }
152921
+ }
152922
+ const signals = (0, import_react.useMemo)(() => {
152923
+ return [...new Set(stats.map((s) => s.signal))].sort();
152924
+ }, [stats]);
152925
+ const globalStats = (0, import_react.useMemo)(() => stats.filter((s) => s.project === null), [stats]);
152926
+ const projectStats = (0, import_react.useMemo)(() => stats.filter((s) => s.project !== null), [stats]);
152927
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
152928
+ className: "space-y-4 p-4",
152929
+ children: [
152930
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
152931
+ className: "flex items-center justify-between",
152932
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
152933
+ className: "flex items-center gap-2",
152934
+ children: [
152935
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Lightbulb, { className: "size-5 text-primary" }),
152936
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", {
152937
+ className: "text-lg font-semibold",
152938
+ children: "Insights"
152939
+ }),
152940
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
152941
+ className: "text-xs text-muted-foreground",
152942
+ children: "continuous self-improvement · epic #183"
152943
+ })
152944
+ ]
152945
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
152946
+ variant: "outline",
152947
+ size: "sm",
152948
+ onClick: () => void refresh(),
152949
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RefreshCw, { className: "size-4" }), " Refresh"]
152950
+ })]
152951
+ }),
152952
+ error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
152953
+ className: "rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive",
152954
+ children: error
152955
+ }),
152956
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(CardTitle, {
152957
+ className: "flex items-center justify-between",
152958
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "Features" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
152959
+ className: "text-xs font-normal text-muted-foreground",
152960
+ children: [
152961
+ "config v",
152962
+ version,
152963
+ " · saved by ",
152964
+ config && "dashboard"
152965
+ ]
152966
+ })]
152967
+ }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(CardContent, {
152968
+ className: "space-y-4",
152969
+ children: [
152970
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
152971
+ className: "flex items-center justify-between",
152972
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
152973
+ className: "text-sm font-medium",
152974
+ children: "Insights enabled"
152975
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
152976
+ className: "text-xs text-muted-foreground",
152977
+ children: "Master switch. When off, no signals are recorded regardless of the toggles below."
152978
+ })] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Switch, {
152979
+ checked: config.enabled,
152980
+ disabled: saving,
152981
+ onCheckedChange: (v) => void saveConfig({
152982
+ ...config,
152983
+ enabled: v
152984
+ })
152985
+ })]
152986
+ }),
152987
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
152988
+ className: "flex items-center justify-between border-t border-border pt-3",
152989
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
152990
+ className: "text-sm font-medium",
152991
+ children: ["Context-gathering ratio ", /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
152992
+ className: "text-xs text-muted-foreground",
152993
+ children: "(#184)"
152994
+ })]
152995
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
152996
+ className: "text-xs text-muted-foreground",
152997
+ children: "Server-side: read/search vs. action before first substantive move, per session."
152998
+ })] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Switch, {
152999
+ checked: config.contextRatio.enabled,
153000
+ disabled: saving || !config.enabled,
153001
+ onCheckedChange: (v) => void saveConfig({
153002
+ ...config,
153003
+ contextRatio: { enabled: v }
153004
+ })
153005
+ })]
153006
+ }),
153007
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
153008
+ className: "border-t border-border pt-3",
153009
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
153010
+ className: "flex items-center justify-between",
153011
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
153012
+ className: "text-sm font-medium",
153013
+ children: ["End-of-session introspection ", /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
153014
+ className: "text-xs text-muted-foreground",
153015
+ children: "(#185)"
153016
+ })]
153017
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153018
+ className: "text-xs text-muted-foreground",
153019
+ children: "Agent-authored 3-field artifact at session end. Gated below."
153020
+ })] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Switch, {
153021
+ checked: config.introspection.enabled,
153022
+ disabled: saving || !config.enabled,
153023
+ onCheckedChange: (v) => void saveConfig({
153024
+ ...config,
153025
+ introspection: {
153026
+ ...config.introspection,
153027
+ enabled: v
153028
+ }
153029
+ })
153030
+ })]
153031
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
153032
+ className: "mt-3 flex flex-wrap gap-4",
153033
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", {
153034
+ className: "flex items-center gap-2 text-xs text-muted-foreground",
153035
+ children: ["min turns", /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
153036
+ type: "number",
153037
+ className: "h-7 w-20",
153038
+ value: config.introspection.minTurns,
153039
+ disabled: saving || !config.enabled || !config.introspection.enabled,
153040
+ onChange: (e) => setConfig({
153041
+ ...config,
153042
+ introspection: {
153043
+ ...config.introspection,
153044
+ minTurns: Number(e.target.value)
153045
+ }
153046
+ }),
153047
+ onBlur: () => void saveConfig(config)
153048
+ })]
153049
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", {
153050
+ className: "flex items-center gap-2 text-xs text-muted-foreground",
153051
+ children: ["min context remaining (0–1)", /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
153052
+ type: "number",
153053
+ step: "0.05",
153054
+ min: "0",
153055
+ max: "1",
153056
+ className: "h-7 w-24",
153057
+ value: config.introspection.minContextRemaining,
153058
+ disabled: saving || !config.enabled || !config.introspection.enabled,
153059
+ onChange: (e) => setConfig({
153060
+ ...config,
153061
+ introspection: {
153062
+ ...config.introspection,
153063
+ minContextRemaining: Number(e.target.value)
153064
+ }
153065
+ }),
153066
+ onBlur: () => void saveConfig(config)
153067
+ })]
153068
+ })]
153069
+ })]
153070
+ })
153071
+ ]
153072
+ })] }),
153073
+ /* @__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", {
153074
+ className: "text-sm text-muted-foreground",
153075
+ children: "No observations yet — they appear after sessions end with Insights enabled."
153076
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153077
+ className: "space-y-4",
153078
+ children: globalStats.map((g) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
153079
+ className: "mb-1 flex items-center gap-2",
153080
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge$1, {
153081
+ variant: "secondary",
153082
+ children: g.signal
153083
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
153084
+ className: "text-xs text-muted-foreground",
153085
+ children: [
153086
+ g.count,
153087
+ " obs · ",
153088
+ g.avgRatio !== null ? `avg ratio ${g.avgRatio.toFixed(2)}` : "no ratio",
153089
+ " · ",
153090
+ timeAgo(now, g.lastAt)
153091
+ ]
153092
+ })]
153093
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153094
+ className: "flex flex-wrap gap-2 pl-2",
153095
+ children: projectStats.filter((p) => p.signal === g.signal).map((p) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
153096
+ className: "rounded border border-border px-2 py-0.5 text-xs text-muted-foreground",
153097
+ children: [
153098
+ p.project,
153099
+ ": ",
153100
+ p.count,
153101
+ p.avgRatio !== null ? ` · ${p.avgRatio.toFixed(2)}` : ""
153102
+ ]
153103
+ }, `${g.signal}-${p.project}`))
153104
+ })] }, g.signal))
153105
+ }) })] }),
153106
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(CardTitle, {
153107
+ className: "flex flex-wrap items-center gap-3",
153108
+ children: [
153109
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "Observations" }),
153110
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
153111
+ className: "h-7 rounded border border-border bg-background px-2 text-xs",
153112
+ value: projectFilter,
153113
+ onChange: (e) => setProjectFilter(e.target.value),
153114
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
153115
+ value: "",
153116
+ children: "all projects"
153117
+ }), projects.map((p) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
153118
+ value: p,
153119
+ children: p
153120
+ }, p))]
153121
+ }),
153122
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
153123
+ className: "h-7 rounded border border-border bg-background px-2 text-xs",
153124
+ value: signalFilter,
153125
+ onChange: (e) => setSignalFilter(e.target.value),
153126
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
153127
+ value: "",
153128
+ children: "all signals"
153129
+ }), signals.map((s) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
153130
+ value: s,
153131
+ children: s
153132
+ }, s))]
153133
+ })
153134
+ ]
153135
+ }) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardContent, { children: observations.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153136
+ className: "text-sm text-muted-foreground",
153137
+ children: "No observations match."
153138
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153139
+ className: "overflow-x-auto",
153140
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", {
153141
+ className: "w-full text-left text-xs",
153142
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", {
153143
+ className: "text-muted-foreground",
153144
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", {
153145
+ className: "border-b border-border",
153146
+ children: [
153147
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
153148
+ className: "py-1 pr-3 font-medium",
153149
+ children: "when"
153150
+ }),
153151
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
153152
+ className: "py-1 pr-3 font-medium",
153153
+ children: "project"
153154
+ }),
153155
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
153156
+ className: "py-1 pr-3 font-medium",
153157
+ children: "signal"
153158
+ }),
153159
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
153160
+ className: "py-1 pr-3 font-medium",
153161
+ children: "src"
153162
+ }),
153163
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
153164
+ className: "py-1 pr-3 font-medium",
153165
+ children: "value"
153166
+ }),
153167
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
153168
+ className: "py-1 pr-3 font-medium",
153169
+ children: "outcome"
153170
+ }),
153171
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("th", {
153172
+ className: "py-1 pr-3 font-medium",
153173
+ children: "session"
153174
+ })
153175
+ ]
153176
+ })
153177
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: observations.map((o) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", {
153178
+ className: "border-b border-border/50",
153179
+ children: [
153180
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
153181
+ className: "py-1 pr-3 whitespace-nowrap text-muted-foreground",
153182
+ children: timeAgo(now, o.createdAt)
153183
+ }),
153184
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
153185
+ className: "py-1 pr-3 whitespace-nowrap",
153186
+ children: o.project
153187
+ }),
153188
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
153189
+ className: "py-1 pr-3 whitespace-nowrap",
153190
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge$1, {
153191
+ variant: "outline",
153192
+ children: o.signal
153193
+ })
153194
+ }),
153195
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
153196
+ className: "py-1 pr-3 whitespace-nowrap text-muted-foreground",
153197
+ children: o.source
153198
+ }),
153199
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
153200
+ className: "py-1 pr-3 font-mono",
153201
+ children: compactJson(o.value)
153202
+ }),
153203
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
153204
+ className: "py-1 pr-3 font-mono text-muted-foreground",
153205
+ children: compactJson(o.outcome)
153206
+ }),
153207
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
153208
+ className: "py-1 pr-3 whitespace-nowrap text-muted-foreground",
153209
+ children: o.agentId ?? o.sessionId.slice(0, 12)
153210
+ })
153211
+ ]
153212
+ }, o.id)) })]
153213
+ })
153214
+ }) })] })
153215
+ ]
153216
+ });
153217
+ }
153218
+ //#endregion
152309
153219
  //#region src/components/views/maintenance.tsx
152310
153220
  var STATUS_CLASS = {
152311
153221
  succeeded: "bg-emerald-500/10 text-emerald-400 border-emerald-500/30",
@@ -156885,6 +157795,7 @@ var views = {
156885
157795
  tasks: TasksView,
156886
157796
  automation: AutomationView,
156887
157797
  analytics: AnalyticsView,
157798
+ insights: InsightsView,
156888
157799
  maintenance: MaintenanceView,
156889
157800
  settings: SettingsView
156890
157801
  };
@@ -157903,6 +158814,11 @@ if ("serviceWorker" in navigator) {
157903
158814
  height: calc(var(--spacing) * 4);
157904
158815
  }
157905
158816
 
158817
+ .size-5 {
158818
+ width: calc(var(--spacing) * 5);
158819
+ height: calc(var(--spacing) * 5);
158820
+ }
158821
+
157906
158822
  .size-6 {
157907
158823
  width: calc(var(--spacing) * 6);
157908
158824
  height: calc(var(--spacing) * 6);
@@ -158224,6 +159140,14 @@ if ("serviceWorker" in navigator) {
158224
159140
  width: calc(var(--spacing) * 14);
158225
159141
  }
158226
159142
 
159143
+ .w-20 {
159144
+ width: calc(var(--spacing) * 20);
159145
+ }
159146
+
159147
+ .w-24 {
159148
+ width: calc(var(--spacing) * 24);
159149
+ }
159150
+
158227
159151
  .w-40 {
158228
159152
  width: calc(var(--spacing) * 40);
158229
159153
  }
@@ -160132,6 +161056,10 @@ if ("serviceWorker" in navigator) {
160132
161056
  padding-top: calc(var(--spacing) * 2);
160133
161057
  }
160134
161058
 
161059
+ .pt-3 {
161060
+ padding-top: calc(var(--spacing) * 3);
161061
+ }
161062
+
160135
161063
  .pt-4 {
160136
161064
  padding-top: calc(var(--spacing) * 4);
160137
161065
  }
@@ -160144,6 +161072,10 @@ if ("serviceWorker" in navigator) {
160144
161072
  padding-right: calc(var(--spacing) * 2);
160145
161073
  }
160146
161074
 
161075
+ .pr-3 {
161076
+ padding-right: calc(var(--spacing) * 3);
161077
+ }
161078
+
160147
161079
  .pr-4 {
160148
161080
  padding-right: calc(var(--spacing) * 4);
160149
161081
  }