agent-relay-server 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-server",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Lightweight HTTP message relay for inter-agent communication across machines",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
package/public/index.html CHANGED
@@ -10510,6 +10510,40 @@ function prepareForSpeech(markdown) {
10510
10510
  }
10511
10511
  //#endregion
10512
10512
  //#region src/lib/voice.ts
10513
+ var KOKORO_VOICES = [
10514
+ {
10515
+ id: "am_michael",
10516
+ label: "Michael (US ♂)"
10517
+ },
10518
+ {
10519
+ id: "am_adam",
10520
+ label: "Adam (US ♂)"
10521
+ },
10522
+ {
10523
+ id: "af_heart",
10524
+ label: "Heart (US ♀)"
10525
+ },
10526
+ {
10527
+ id: "af_bella",
10528
+ label: "Bella (US ♀)"
10529
+ },
10530
+ {
10531
+ id: "af_nicole",
10532
+ label: "Nicole (US ♀)"
10533
+ },
10534
+ {
10535
+ id: "af_sarah",
10536
+ label: "Sarah (US ♀)"
10537
+ },
10538
+ {
10539
+ id: "bm_george",
10540
+ label: "George (UK ♂)"
10541
+ },
10542
+ {
10543
+ id: "bf_emma",
10544
+ label: "Emma (UK ♀)"
10545
+ }
10546
+ ];
10513
10547
  var MAX_CHUNK = 220;
10514
10548
  /** Split into utterance-sized chunks (sentence boundaries; hard-split very long runs). */
10515
10549
  function chunkForSpeech(text) {
@@ -10537,6 +10571,7 @@ var VoiceTts = class {
10537
10571
  lang = "en-US";
10538
10572
  mode = "kokoro";
10539
10573
  kokoroVoice = "am_michael";
10574
+ browserVoice = "";
10540
10575
  active = null;
10541
10576
  queue = [];
10542
10577
  currentChat = null;
@@ -10586,6 +10621,10 @@ var VoiceTts = class {
10586
10621
  setKokoroVoice(voice) {
10587
10622
  this.kokoroVoice = voice;
10588
10623
  }
10624
+ /** Set the browser engine's voice by voiceURI. Empty = default for the language. */
10625
+ setBrowserVoice(uri) {
10626
+ this.browserVoice = uri;
10627
+ }
10589
10628
  setActiveChat(chatId) {
10590
10629
  if (chatId === this.active) return;
10591
10630
  this.active = chatId;
@@ -10683,7 +10722,11 @@ var VoiceTts = class {
10683
10722
  if (gen !== this.gen) return;
10684
10723
  if (!synthAvailable || i >= chunks.length) return done();
10685
10724
  const u = new SpeechSynthesisUtterance(chunks[i]);
10686
- u.lang = this.lang || navigator.language || "en-US";
10725
+ const picked = this.browserVoice ? window.speechSynthesis.getVoices().find((v) => v.voiceURI === this.browserVoice) : void 0;
10726
+ if (picked) {
10727
+ u.voice = picked;
10728
+ u.lang = picked.lang;
10729
+ } else u.lang = this.lang || navigator.language || "en-US";
10687
10730
  u.onend = () => this.speakBrowser(chunks, i + 1, gen, done);
10688
10731
  u.onerror = () => this.speakBrowser(chunks, i + 1, gen, done);
10689
10732
  window.speechSynthesis.speak(u);
@@ -10732,12 +10775,16 @@ var VoiceTts = class {
10732
10775
  }
10733
10776
  };
10734
10777
  var voiceTts = new VoiceTts();
10735
- /** Sorted unique BCP-47 languages the browser's speech engine can speak. May be empty
10736
- * until the engine finishes loading voices (listen for `voiceschanged` and re-read). */
10737
- function availableSpeechLangs() {
10778
+ /** The browser's individual speech voices ({uri, label, lang}), sorted by language
10779
+ * then name. May be empty until the engine loads (listen for `voiceschanged`). */
10780
+ function availableSpeechVoices() {
10738
10781
  if (!synthAvailable) return [];
10739
10782
  try {
10740
- return [...new Set(window.speechSynthesis.getVoices().map((v) => v.lang).filter(Boolean))].sort();
10783
+ return window.speechSynthesis.getVoices().map((v) => ({
10784
+ uri: v.voiceURI,
10785
+ label: `${v.name} (${v.lang})`,
10786
+ lang: v.lang
10787
+ })).sort((a, b) => a.lang.localeCompare(b.lang) || a.label.localeCompare(b.label));
10741
10788
  } catch {
10742
10789
  return [];
10743
10790
  }
@@ -12176,6 +12223,7 @@ var useRelayStore = create$1()(persist((set, get) => ({
12176
12223
  voiceTtsLang: "en-US",
12177
12224
  voiceTtsMode: "kokoro",
12178
12225
  voiceTtsKokoroVoice: "am_michael",
12226
+ voiceTtsBrowserVoice: "",
12179
12227
  voiceInputMode: "compose",
12180
12228
  agentSort: "status",
12181
12229
  agentSortDir: "asc",
@@ -12220,6 +12268,8 @@ var useRelayStore = create$1()(persist((set, get) => ({
12220
12268
  chatAgentHostFilter: "",
12221
12269
  chatAgentSort: "status",
12222
12270
  chatAgentSortDir: "asc",
12271
+ chatAgentGroupBy: "",
12272
+ chatAgentFiltersCollapsed: true,
12223
12273
  chatStatusEvents: {},
12224
12274
  chatStickToBottom: true,
12225
12275
  chatHasNewItems: false,
@@ -12382,6 +12432,10 @@ var useRelayStore = create$1()(persist((set, get) => ({
12382
12432
  voiceTts.setKokoroVoice(voice);
12383
12433
  set({ voiceTtsKokoroVoice: voice });
12384
12434
  },
12435
+ setVoiceTtsBrowserVoice(uri) {
12436
+ voiceTts.setBrowserVoice(uri);
12437
+ set({ voiceTtsBrowserVoice: uri });
12438
+ },
12385
12439
  setVoiceInputMode(mode) {
12386
12440
  set({ voiceInputMode: mode });
12387
12441
  },
@@ -12398,6 +12452,7 @@ var useRelayStore = create$1()(persist((set, get) => ({
12398
12452
  voiceTts.setLang(get().voiceTtsLang);
12399
12453
  voiceTts.setMode(get().voiceTtsMode);
12400
12454
  voiceTts.setKokoroVoice(get().voiceTtsKokoroVoice);
12455
+ voiceTts.setBrowserVoice(get().voiceTtsBrowserVoice);
12401
12456
  syncVoiceActiveChat(get());
12402
12457
  setUnauthorizedHandler(() => {
12403
12458
  if (!get().authNeeded) set({
@@ -14520,6 +14575,7 @@ var useRelayStore = create$1()(persist((set, get) => ({
14520
14575
  voiceTtsLang: state.voiceTtsLang,
14521
14576
  voiceTtsMode: state.voiceTtsMode,
14522
14577
  voiceTtsKokoroVoice: state.voiceTtsKokoroVoice,
14578
+ voiceTtsBrowserVoice: state.voiceTtsBrowserVoice,
14523
14579
  voiceInputMode: state.voiceInputMode,
14524
14580
  agentSort: state.agentSort,
14525
14581
  agentSortDir: state.agentSortDir,
@@ -14538,6 +14594,8 @@ var useRelayStore = create$1()(persist((set, get) => ({
14538
14594
  chatAgentHostFilter: state.chatAgentHostFilter,
14539
14595
  chatAgentSort: state.chatAgentSort,
14540
14596
  chatAgentSortDir: state.chatAgentSortDir,
14597
+ chatAgentGroupBy: state.chatAgentGroupBy,
14598
+ chatAgentFiltersCollapsed: state.chatAgentFiltersCollapsed,
14541
14599
  activityFilter: state.activityFilter,
14542
14600
  analyticsPeriod: state.analyticsPeriod,
14543
14601
  memoryFilters: state.memoryFilters,
@@ -108818,7 +108876,7 @@ function ContextMeter({ agent }) {
108818
108876
  function runtimeChips(agent) {
108819
108877
  const spawned = isRelaySpawned(agent);
108820
108878
  const approval = approvalMode(agent);
108821
- const workspace = workspaceMode(agent);
108879
+ const workspace = agentWorkspaceMeta(agent);
108822
108880
  return [
108823
108881
  {
108824
108882
  key: "origin",
@@ -108858,7 +108916,7 @@ function approvalMode(agent) {
108858
108916
  className: "border-amber-500/30 bg-amber-500/10 text-amber-300"
108859
108917
  };
108860
108918
  }
108861
- function workspaceMode(agent) {
108919
+ function agentWorkspaceMeta(agent) {
108862
108920
  const workspace = recordValue(agent.meta?.workspace);
108863
108921
  const requested = stringValue(workspace.requestedMode) || stringValue(agent.meta?.workspaceMode);
108864
108922
  const effective = stringValue(workspace.mode);
@@ -108868,6 +108926,7 @@ function workspaceMode(agent) {
108868
108926
  label: "Isolated",
108869
108927
  title: "Workspace: isolated worktree",
108870
108928
  Icon: GitBranch,
108929
+ iconColor: "text-violet-300",
108871
108930
  className: "border-violet-500/30 bg-violet-500/10 text-violet-300"
108872
108931
  };
108873
108932
  if (mode === "shared") return {
@@ -108875,6 +108934,7 @@ function workspaceMode(agent) {
108875
108934
  label: "Shared",
108876
108935
  title: "Workspace: shared repo checkout",
108877
108936
  Icon: Share2,
108937
+ iconColor: "text-cyan-300",
108878
108938
  className: "border-cyan-500/30 bg-cyan-500/10 text-cyan-300"
108879
108939
  };
108880
108940
  return {
@@ -108882,12 +108942,18 @@ function workspaceMode(agent) {
108882
108942
  label: "Default",
108883
108943
  title: effective ? `Workspace: default/inherit, resolved to ${effective}` : "Workspace: default/inherit",
108884
108944
  Icon: Layers,
108945
+ iconColor: "text-zinc-400",
108885
108946
  className: "border-zinc-500/30 bg-zinc-500/10 text-zinc-300"
108886
108947
  };
108887
108948
  }
108888
108949
  function recordValue(value) {
108889
108950
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
108890
108951
  }
108952
+ function agentProjectName(agent) {
108953
+ const workspace = recordValue(agent.meta?.workspace);
108954
+ const root = stringValue(workspace.repoRoot) || stringValue(workspace.sourceCwd) || stringValue(agent.meta?.cwd);
108955
+ return root ? shortPath$1(root, 2) : "";
108956
+ }
108891
108957
  function runtimeBadges(agent) {
108892
108958
  const caps = agent.providerCapabilities;
108893
108959
  const context = agent.context;
@@ -108903,40 +108969,47 @@ function runtimeBadges(agent) {
108903
108969
  if (context) badges.push(`${Math.round(context.utilization * 100)}% ctx`);
108904
108970
  return badges;
108905
108971
  }
108906
- function ContextRing({ utilization, className }) {
108972
+ function ContextRing({ utilization, size = 26, className }) {
108907
108973
  const pct = Math.round(Math.min(1, Math.max(0, utilization)) * 100);
108908
- const r = 10;
108974
+ const stroke = size >= 24 ? 3 : 2.5;
108975
+ const c = size / 2;
108976
+ const r = c - stroke;
108909
108977
  const circumference = 2 * Math.PI * r;
108910
108978
  const offset = circumference * (1 - Math.min(1, Math.max(0, utilization)));
108911
108979
  const color = utilization > .8 ? "text-red-400" : utilization > .5 ? "text-yellow-400" : "text-emerald-400";
108912
108980
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
108913
108981
  className: `relative shrink-0 ${className ?? ""}`,
108982
+ style: {
108983
+ width: size,
108984
+ height: size
108985
+ },
108914
108986
  title: `Context: ${pct}%`,
108915
108987
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", {
108916
- width: "26",
108917
- height: "26",
108918
- viewBox: "0 0 26 26",
108988
+ width: size,
108989
+ height: size,
108990
+ viewBox: `0 0 ${size} ${size}`,
108919
108991
  className: "rotate-[-90deg]",
108920
108992
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", {
108921
- cx: "13",
108922
- cy: "13",
108993
+ cx: c,
108994
+ cy: c,
108923
108995
  r,
108924
108996
  fill: "none",
108925
- strokeWidth: "3",
108997
+ strokeWidth: stroke,
108926
108998
  className: "stroke-muted-foreground/20"
108927
108999
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", {
108928
- cx: "13",
108929
- cy: "13",
109000
+ cx: c,
109001
+ cy: c,
108930
109002
  r,
108931
109003
  fill: "none",
108932
- strokeWidth: "3",
109004
+ strokeWidth: stroke,
108933
109005
  strokeDasharray: circumference,
108934
109006
  strokeDashoffset: offset,
108935
109007
  strokeLinecap: "round",
108936
109008
  className: `${color} stroke-current transition-all duration-500`
108937
109009
  })]
108938
109010
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
108939
- className: `absolute inset-0 flex items-center justify-center text-[8px] font-semibold leading-none ${color}`,
109011
+ className: `absolute inset-0 flex items-center justify-center font-semibold leading-none ${color}`,
109012
+ style: { fontSize: size >= 24 ? 8 : 7 },
108940
109013
  children: pct
108941
109014
  })]
108942
109015
  });
@@ -125643,6 +125716,8 @@ function AgentListPanel({ threads, onSelectAgent }) {
125643
125716
  const chatAgentTagFilter = useRelayStore((s) => s.chatAgentTagFilter);
125644
125717
  const chatAgentCapFilter = useRelayStore((s) => s.chatAgentCapFilter);
125645
125718
  const chatAgentHostFilter = useRelayStore((s) => s.chatAgentHostFilter);
125719
+ const chatAgentGroupBy = useRelayStore((s) => s.chatAgentGroupBy);
125720
+ const chatAgentFiltersCollapsed = useRelayStore((s) => s.chatAgentFiltersCollapsed);
125646
125721
  const now = useNow();
125647
125722
  const chatAgents = useChatAgents();
125648
125723
  const threadByPeer = (0, import_react.useMemo)(() => {
@@ -125666,161 +125741,287 @@ function AgentListPanel({ threads, onSelectAgent }) {
125666
125741
  const uniqueTags = (0, import_react.useMemo)(() => [...new Set(chatAgents.flatMap((a) => a.tags || []))], [chatAgents]);
125667
125742
  const uniqueCaps = (0, import_react.useMemo)(() => [...new Set(chatAgents.flatMap((a) => userFacingCapabilities(a.capabilities || [])))], [chatAgents]);
125668
125743
  const uniqueHosts = useUniqueHosts();
125744
+ const groupedAgents = (0, import_react.useMemo)(() => {
125745
+ if (chatAgentGroupBy !== "project") return null;
125746
+ const groups = /* @__PURE__ */ new Map();
125747
+ for (const a of sortedAgents) {
125748
+ const key = agentProjectName(a) || "No project";
125749
+ const bucket = groups.get(key);
125750
+ if (bucket) bucket.push(a);
125751
+ else groups.set(key, [a]);
125752
+ }
125753
+ return [...groups.entries()].sort((a, b) => a[0].localeCompare(b[0]));
125754
+ }, [sortedAgents, chatAgentGroupBy]);
125755
+ const activeFilterCount = [
125756
+ chatAgentProviderFilter,
125757
+ chatAgentStatusFilter,
125758
+ chatAgentTagFilter,
125759
+ chatAgentCapFilter,
125760
+ chatAgentHostFilter
125761
+ ].filter(Boolean).length;
125669
125762
  function handleSelect(id) {
125670
125763
  openInboxThread(id, threadByPeer.get(id)?.messages);
125671
125764
  onSelectAgent?.(id);
125672
125765
  }
125673
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125674
- className: "flex flex-col h-full border-r border-border min-w-0",
125675
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125676
- className: "p-3 border-b border-border space-y-2",
125677
- children: [
125678
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125679
- className: "relative",
125680
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Search, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground pointer-events-none" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
125681
- placeholder: "Search agents...",
125682
- value: chatAgentSearch,
125683
- onChange: (e) => set({ chatAgentSearch: e.target.value }),
125684
- className: "pl-8 h-7 text-xs"
125685
- })]
125686
- }),
125687
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125688
- className: "flex items-center gap-1.5",
125689
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
125690
- value: chatAgentSort,
125691
- onChange: (e) => set({ chatAgentSort: e.target.value }),
125692
- className: "flex-1 h-6 text-xs rounded-md border border-border bg-background px-1.5 text-foreground",
125766
+ function renderAgentRow(agent) {
125767
+ const thread = threadByPeer.get(agent.id);
125768
+ const unread = thread?.attention.unread || 0;
125769
+ const lastMsg = thread?.previewMessage;
125770
+ const lastActivityAt = threadActivityTimestamp(thread);
125771
+ const isSelected = selectedInboxThread === agent.id;
125772
+ const ws = agentWorkspaceMeta(agent);
125773
+ const project = agentProjectName(agent);
125774
+ const WsIcon = ws.Icon;
125775
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
125776
+ 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", isSelected && "bg-muted/60"),
125777
+ onClick: () => handleSelect(agent.id),
125778
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125779
+ className: "relative shrink-0 mt-0.5",
125780
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AgentTypeIcon, { agent }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusDot, {
125781
+ agent,
125782
+ now,
125783
+ className: "absolute -bottom-0.5 -right-0.5 w-2 h-2"
125784
+ })]
125785
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125786
+ className: "flex-1 min-w-0",
125787
+ children: [
125788
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125789
+ className: "flex items-center justify-between gap-1 mb-0.5",
125790
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
125791
+ className: "text-xs font-medium truncate",
125792
+ children: displayName(agent)
125793
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125794
+ className: "flex items-center gap-1 shrink-0",
125795
+ children: [lastActivityAt > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
125796
+ className: "text-[10px] text-muted-foreground",
125797
+ title: fmtTime$1(lastActivityAt),
125798
+ children: timeAgo(now, lastActivityAt)
125799
+ }), unread > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge$1, {
125800
+ className: "bg-red-500 text-white text-xs min-w-[18px] h-[18px] flex items-center justify-center shrink-0 px-1",
125801
+ children: unread
125802
+ })]
125803
+ })]
125804
+ }),
125805
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125806
+ className: "flex items-center gap-1.5 mb-0.5 text-[10px] text-muted-foreground",
125693
125807
  children: [
125694
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125695
- value: "status",
125696
- children: "By status"
125697
- }),
125698
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125699
- value: "name",
125700
- children: "By name"
125808
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
125809
+ title: ws.title,
125810
+ className: "inline-flex shrink-0",
125811
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(WsIcon, { className: cn$2("h-3 w-3", ws.iconColor) })
125701
125812
  }),
125702
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125703
- value: "lastSeen",
125704
- children: "By last seen"
125813
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
125814
+ className: "truncate",
125815
+ children: project || ws.label
125705
125816
  }),
125706
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125707
- value: "lastMessage",
125708
- children: "By last activity"
125817
+ agent.context && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
125818
+ className: "ml-auto shrink-0",
125819
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ContextRing, {
125820
+ utilization: agent.context.utilization,
125821
+ size: 18
125822
+ })
125709
125823
  })
125710
125824
  ]
125711
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
125712
- variant: "ghost",
125713
- size: "icon-xs",
125714
- onClick: () => set({ chatAgentSortDir: chatAgentSortDir === "asc" ? "desc" : "asc" }),
125715
- children: chatAgentSortDir === "asc" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ArrowUpNarrowWide, { className: "w-3.5 h-3.5" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ArrowDownWideNarrow, { className: "w-3.5 h-3.5" })
125716
- })]
125717
- }),
125718
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125719
- className: "flex gap-1",
125720
- children: [
125721
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
125722
- value: chatAgentProviderFilter,
125723
- onChange: (e) => set({ chatAgentProviderFilter: e.target.value }),
125724
- className: "flex-1 h-6 text-xs rounded-md border border-border bg-background px-1.5 text-foreground",
125825
+ }),
125826
+ lastMsg && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
125827
+ className: "text-xs text-muted-foreground truncate leading-tight",
125828
+ children: [lastMsg.from === "user" ? "You: " : "", messagePreview(lastMsg)]
125829
+ }),
125830
+ agent.status !== "offline" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
125831
+ className: "mt-1",
125832
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
125833
+ className: cn$2("text-xs", agent.status === "busy" ? "text-yellow-400" : "text-emerald-400"),
125834
+ children: agent.status
125835
+ })
125836
+ })
125837
+ ]
125838
+ })]
125839
+ }) }, agent.id);
125840
+ }
125841
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125842
+ className: "flex flex-col h-full border-r border-border min-w-0",
125843
+ children: [
125844
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125845
+ className: "p-3 border-b border-border space-y-2",
125846
+ children: [
125847
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125848
+ className: "relative",
125849
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Search, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground pointer-events-none" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
125850
+ placeholder: "Search agents...",
125851
+ value: chatAgentSearch,
125852
+ onChange: (e) => set({ chatAgentSearch: e.target.value }),
125853
+ className: "pl-8 h-7 text-xs"
125854
+ })]
125855
+ }),
125856
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
125857
+ type: "button",
125858
+ onClick: () => set({ chatAgentFiltersCollapsed: !chatAgentFiltersCollapsed }),
125859
+ className: "sm:hidden flex w-full items-center justify-between h-6 px-1.5 text-xs rounded-md border border-border bg-background text-muted-foreground",
125860
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
125861
+ className: "inline-flex items-center gap-1.5",
125725
125862
  children: [
125726
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125727
- value: "",
125728
- children: "All types"
125729
- }),
125730
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125731
- value: "claude",
125732
- children: "Claude"
125733
- }),
125734
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125735
- value: "codex",
125736
- children: "Codex"
125737
- }),
125738
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125739
- value: "agent",
125740
- children: "Agent"
125741
- }),
125742
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125743
- value: "user",
125744
- children: "User"
125863
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SlidersHorizontal, { className: "w-3.5 h-3.5" }),
125864
+ "Filters & sort",
125865
+ activeFilterCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge$1, {
125866
+ className: "bg-primary/20 text-primary text-[10px] h-4 min-w-4 px-1 flex items-center justify-center",
125867
+ children: activeFilterCount
125745
125868
  })
125746
125869
  ]
125747
- }),
125748
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
125749
- value: chatAgentStatusFilter,
125750
- onChange: (e) => set({ chatAgentStatusFilter: e.target.value }),
125751
- className: "flex-1 h-6 text-xs rounded-md border border-border bg-background px-1.5 text-foreground",
125752
- children: [
125753
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125870
+ }), chatAgentFiltersCollapsed ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronDown, { className: "w-3.5 h-3.5" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronUp, { className: "w-3.5 h-3.5" })]
125871
+ }),
125872
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125873
+ className: cn$2("space-y-2", chatAgentFiltersCollapsed ? "hidden" : "block", "sm:block"),
125874
+ children: [
125875
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125876
+ className: "flex items-center gap-1.5",
125877
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
125878
+ value: chatAgentSort,
125879
+ onChange: (e) => set({ chatAgentSort: e.target.value }),
125880
+ className: "flex-1 h-6 text-xs rounded-md border border-border bg-background px-1.5 text-foreground",
125881
+ children: [
125882
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125883
+ value: "status",
125884
+ children: "By status"
125885
+ }),
125886
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125887
+ value: "name",
125888
+ children: "By name"
125889
+ }),
125890
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125891
+ value: "lastSeen",
125892
+ children: "By last seen"
125893
+ }),
125894
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125895
+ value: "lastMessage",
125896
+ children: "By last activity"
125897
+ })
125898
+ ]
125899
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
125900
+ variant: "ghost",
125901
+ size: "icon-xs",
125902
+ onClick: () => set({ chatAgentSortDir: chatAgentSortDir === "asc" ? "desc" : "asc" }),
125903
+ children: chatAgentSortDir === "asc" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ArrowUpNarrowWide, { className: "w-3.5 h-3.5" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ArrowDownWideNarrow, { className: "w-3.5 h-3.5" })
125904
+ })]
125905
+ }),
125906
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
125907
+ value: chatAgentGroupBy,
125908
+ onChange: (e) => set({ chatAgentGroupBy: e.target.value }),
125909
+ className: "w-full h-6 text-xs rounded-md border border-border bg-background px-1.5 text-foreground",
125910
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125754
125911
  value: "",
125755
- children: "All status"
125756
- }),
125757
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125758
- value: "online",
125759
- children: "Online"
125760
- }),
125761
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125762
- value: "idle",
125763
- children: "Idle"
125764
- }),
125765
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125766
- value: "busy",
125767
- children: "Busy"
125768
- }),
125769
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125770
- value: "offline",
125771
- children: "Offline"
125772
- })
125773
- ]
125774
- }),
125775
- uniqueHosts.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
125776
- value: chatAgentHostFilter,
125777
- onChange: (e) => set({ chatAgentHostFilter: e.target.value }),
125778
- className: "flex-1 h-6 text-xs rounded-md border border-border bg-background px-1.5 text-foreground",
125779
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125780
- value: "",
125781
- children: "All hosts"
125782
- }), uniqueHosts.map((h) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125783
- value: h,
125784
- children: h
125785
- }, h))]
125786
- })
125787
- ]
125788
- }),
125789
- (uniqueTags.length > 0 || uniqueCaps.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125790
- className: "flex gap-1",
125791
- children: [uniqueTags.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
125792
- value: chatAgentTagFilter,
125793
- onChange: (e) => set({ chatAgentTagFilter: e.target.value }),
125794
- className: "flex-1 h-6 text-xs rounded-md border border-border bg-background px-1.5 text-foreground",
125795
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125796
- value: "",
125797
- children: "All tags"
125798
- }), uniqueTags.map((t) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("option", {
125799
- value: t,
125800
- children: ["#", t]
125801
- }, t))]
125802
- }), uniqueCaps.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
125803
- value: chatAgentCapFilter,
125804
- onChange: (e) => set({ chatAgentCapFilter: e.target.value }),
125805
- className: "flex-1 h-6 text-xs rounded-md border border-border bg-background px-1.5 text-foreground",
125806
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125807
- value: "",
125808
- children: "All caps"
125809
- }), uniqueCaps.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125810
- value: c,
125811
- children: c
125812
- }, c))]
125813
- })]
125814
- })
125815
- ]
125816
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125817
- className: "flex-1 overflow-y-auto",
125818
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
125912
+ children: "No grouping"
125913
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125914
+ value: "project",
125915
+ children: "Group by project"
125916
+ })]
125917
+ }),
125918
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125919
+ className: "flex gap-1",
125920
+ children: [
125921
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
125922
+ value: chatAgentProviderFilter,
125923
+ onChange: (e) => set({ chatAgentProviderFilter: e.target.value }),
125924
+ className: "flex-1 h-6 text-xs rounded-md border border-border bg-background px-1.5 text-foreground",
125925
+ children: [
125926
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125927
+ value: "",
125928
+ children: "All types"
125929
+ }),
125930
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125931
+ value: "claude",
125932
+ children: "Claude"
125933
+ }),
125934
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125935
+ value: "codex",
125936
+ children: "Codex"
125937
+ }),
125938
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125939
+ value: "agent",
125940
+ children: "Agent"
125941
+ }),
125942
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125943
+ value: "user",
125944
+ children: "User"
125945
+ })
125946
+ ]
125947
+ }),
125948
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
125949
+ value: chatAgentStatusFilter,
125950
+ onChange: (e) => set({ chatAgentStatusFilter: e.target.value }),
125951
+ className: "flex-1 h-6 text-xs rounded-md border border-border bg-background px-1.5 text-foreground",
125952
+ children: [
125953
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125954
+ value: "",
125955
+ children: "All status"
125956
+ }),
125957
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125958
+ value: "online",
125959
+ children: "Online"
125960
+ }),
125961
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125962
+ value: "idle",
125963
+ children: "Idle"
125964
+ }),
125965
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125966
+ value: "busy",
125967
+ children: "Busy"
125968
+ }),
125969
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125970
+ value: "offline",
125971
+ children: "Offline"
125972
+ })
125973
+ ]
125974
+ }),
125975
+ uniqueHosts.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
125976
+ value: chatAgentHostFilter,
125977
+ onChange: (e) => set({ chatAgentHostFilter: e.target.value }),
125978
+ className: "flex-1 h-6 text-xs rounded-md border border-border bg-background px-1.5 text-foreground",
125979
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125980
+ value: "",
125981
+ children: "All hosts"
125982
+ }), uniqueHosts.map((h) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125983
+ value: h,
125984
+ children: h
125985
+ }, h))]
125986
+ })
125987
+ ]
125988
+ }),
125989
+ (uniqueTags.length > 0 || uniqueCaps.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125990
+ className: "flex gap-1",
125991
+ children: [uniqueTags.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
125992
+ value: chatAgentTagFilter,
125993
+ onChange: (e) => set({ chatAgentTagFilter: e.target.value }),
125994
+ className: "flex-1 h-6 text-xs rounded-md border border-border bg-background px-1.5 text-foreground",
125995
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125996
+ value: "",
125997
+ children: "All tags"
125998
+ }), uniqueTags.map((t) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("option", {
125999
+ value: t,
126000
+ children: ["#", t]
126001
+ }, t))]
126002
+ }), uniqueCaps.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
126003
+ value: chatAgentCapFilter,
126004
+ onChange: (e) => set({ chatAgentCapFilter: e.target.value }),
126005
+ className: "flex-1 h-6 text-xs rounded-md border border-border bg-background px-1.5 text-foreground",
126006
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
126007
+ value: "",
126008
+ children: "All caps"
126009
+ }), uniqueCaps.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
126010
+ value: c,
126011
+ children: c
126012
+ }, c))]
126013
+ })]
126014
+ })
126015
+ ]
126016
+ })
126017
+ ]
126018
+ }),
126019
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
125819
126020
  type: "button",
125820
- className: "flex w-full items-start gap-2.5 border-b border-border/50 px-3 py-2.5 text-left transition-colors hover:bg-muted/50",
126021
+ className: "flex w-full shrink-0 items-center gap-2.5 border-b border-border px-3 py-2 text-left transition-colors hover:bg-muted/50",
125821
126022
  onClick: openAgentSpawn,
125822
126023
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
125823
- className: "mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-dashed border-primary/50 bg-primary/10 text-primary",
126024
+ className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-dashed border-primary/50 bg-primary/10 text-primary",
125824
126025
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Plus, { className: "h-3.5 w-3.5" })
125825
126026
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125826
126027
  className: "min-w-0 flex-1",
@@ -125828,71 +126029,31 @@ function AgentListPanel({ threads, onSelectAgent }) {
125828
126029
  className: "text-xs font-medium",
125829
126030
  children: "New Agent"
125830
126031
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
125831
- className: "mt-0.5 truncate text-xs text-muted-foreground",
126032
+ className: "truncate text-[10px] text-muted-foreground",
125832
126033
  children: "Spawn a fresh chat agent"
125833
126034
  })]
125834
126035
  })]
125835
- }), sortedAgents.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125836
- className: "flex flex-col items-center justify-center py-10 text-center px-4",
125837
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Bot, { className: "w-8 h-8 text-zinc-600 mb-2" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
125838
- className: "text-xs text-muted-foreground",
125839
- children: "No agents match filters"
125840
- })]
125841
- }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { children: sortedAgents.map((agent) => {
125842
- const thread = threadByPeer.get(agent.id);
125843
- const unread = thread?.attention.unread || 0;
125844
- const lastMsg = thread?.previewMessage;
125845
- const lastActivityAt = threadActivityTimestamp(thread);
125846
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
125847
- 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"),
125848
- onClick: () => handleSelect(agent.id),
125849
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125850
- className: "relative shrink-0 mt-0.5",
125851
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AgentTypeIcon, { agent }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusDot, {
125852
- agent,
125853
- now,
125854
- className: "absolute -bottom-0.5 -right-0.5 w-2 h-2"
125855
- })]
125856
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125857
- className: "flex-1 min-w-0",
125858
- children: [
125859
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125860
- className: "flex items-center justify-between gap-1 mb-0.5",
125861
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
125862
- className: "text-xs font-medium truncate",
125863
- children: displayName(agent)
125864
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125865
- className: "flex items-center gap-1 shrink-0",
125866
- children: [lastActivityAt > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
125867
- className: "text-[10px] text-muted-foreground",
125868
- title: fmtTime$1(lastActivityAt),
125869
- children: timeAgo(now, lastActivityAt)
125870
- }), unread > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge$1, {
125871
- className: "bg-red-500 text-white text-xs min-w-[18px] h-[18px] flex items-center justify-center shrink-0 px-1",
125872
- children: unread
125873
- })]
125874
- })]
125875
- }),
125876
- typeof agent.meta?.cwd === "string" && agent.meta.cwd && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
125877
- className: "font-mono text-[10px] text-muted-foreground/60 truncate",
125878
- children: shortPath$1(agent.meta.cwd)
125879
- }),
125880
- lastMsg && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
125881
- className: "text-xs text-muted-foreground truncate leading-tight",
125882
- children: [lastMsg.from === "user" ? "You: " : "", messagePreview(lastMsg)]
125883
- }),
125884
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
125885
- className: "flex flex-wrap gap-1 mt-1",
125886
- children: agent.status !== "offline" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
125887
- className: cn$2("text-xs", agent.status === "busy" ? "text-yellow-400" : agent.status === "idle" ? "text-emerald-400" : "text-emerald-400"),
125888
- children: agent.status
125889
- })
125890
- })
125891
- ]
126036
+ }),
126037
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
126038
+ className: "flex-1 overflow-y-auto",
126039
+ children: sortedAgents.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
126040
+ className: "flex flex-col items-center justify-center py-10 text-center px-4",
126041
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Bot, { className: "w-8 h-8 text-zinc-600 mb-2" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
126042
+ className: "text-xs text-muted-foreground",
126043
+ children: "No agents match filters"
125892
126044
  })]
125893
- }) }, agent.id);
125894
- }) })]
125895
- })]
126045
+ }) : groupedAgents ? groupedAgents.map(([project, agents]) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
126046
+ className: "sticky top-0 z-10 flex items-center gap-1.5 border-b border-border/50 bg-background/95 px-3 py-1 text-[10px] font-medium uppercase tracking-wide text-muted-foreground backdrop-blur",
126047
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
126048
+ className: "truncate",
126049
+ children: project
126050
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
126051
+ className: "text-muted-foreground/50",
126052
+ children: agents.length
126053
+ })]
126054
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { children: agents.map(renderAgentRow) })] }, project)) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { children: sortedAgents.map(renderAgentRow) })
126055
+ })
126056
+ ]
125896
126057
  });
125897
126058
  }
125898
126059
  function chatTimestamp(iso) {
@@ -125952,40 +126113,6 @@ var TIMELINE_STATUS_LABELS = {
125952
126113
  var TIMELINE_STATUSES = new Set(Object.keys(TIMELINE_STATUS_LABELS));
125953
126114
  var STATUS_DEDUPE_WINDOW_MS = 3e3;
125954
126115
  var CHAT_BOTTOM_THRESHOLD_PX = 96;
125955
- var KOKORO_VOICES = [
125956
- {
125957
- id: "am_michael",
125958
- label: "Michael (US ♂)"
125959
- },
125960
- {
125961
- id: "am_adam",
125962
- label: "Adam (US ♂)"
125963
- },
125964
- {
125965
- id: "af_heart",
125966
- label: "Heart (US ♀)"
125967
- },
125968
- {
125969
- id: "af_bella",
125970
- label: "Bella (US ♀)"
125971
- },
125972
- {
125973
- id: "af_nicole",
125974
- label: "Nicole (US ♀)"
125975
- },
125976
- {
125977
- id: "af_sarah",
125978
- label: "Sarah (US ♀)"
125979
- },
125980
- {
125981
- id: "bm_george",
125982
- label: "George (UK ♂)"
125983
- },
125984
- {
125985
- id: "bf_emma",
125986
- label: "Emma (UK ♀)"
125987
- }
125988
- ];
125989
126116
  function StatusMarker({ event }) {
125990
126117
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125991
126118
  className: "flex items-center justify-center gap-2 py-2 my-1",
@@ -127145,21 +127272,7 @@ function ChatPanel({ threads, onBack, showBackButton }) {
127145
127272
  const fetchOrchestrators = useRelayStore((s) => s.fetchOrchestrators);
127146
127273
  const voiceTtsEnabled = useRelayStore((s) => s.voiceTtsEnabled);
127147
127274
  const setVoiceTtsEnabled = useRelayStore((s) => s.setVoiceTtsEnabled);
127148
- const voiceTtsLang = useRelayStore((s) => s.voiceTtsLang);
127149
- const setVoiceTtsLang = useRelayStore((s) => s.setVoiceTtsLang);
127150
- const voiceTtsMode = useRelayStore((s) => s.voiceTtsMode);
127151
- const setVoiceTtsMode = useRelayStore((s) => s.setVoiceTtsMode);
127152
- const voiceTtsKokoroVoice = useRelayStore((s) => s.voiceTtsKokoroVoice);
127153
- const setVoiceTtsKokoroVoice = useRelayStore((s) => s.setVoiceTtsKokoroVoice);
127154
127275
  const voiceInputMode = useRelayStore((s) => s.voiceInputMode);
127155
- const [speechLangs, setSpeechLangs] = (0, import_react.useState)(() => availableSpeechLangs());
127156
- (0, import_react.useEffect)(() => {
127157
- if (!voiceTts.available) return;
127158
- const refresh = () => setSpeechLangs(availableSpeechLangs());
127159
- refresh();
127160
- window.speechSynthesis.addEventListener?.("voiceschanged", refresh);
127161
- return () => window.speechSynthesis.removeEventListener?.("voiceschanged", refresh);
127162
- }, []);
127163
127276
  const fileInputRef = (0, import_react.useRef)(null);
127164
127277
  const pttRecorderRef = (0, import_react.useRef)(null);
127165
127278
  const [micState, setMicState] = (0, import_react.useState)("idle");
@@ -127583,52 +127696,11 @@ function ChatPanel({ threads, onBack, showBackButton }) {
127583
127696
  voiceTts.available && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
127584
127697
  variant: "ghost",
127585
127698
  size: "icon-sm",
127586
- title: voiceTtsEnabled ? "Speaking agent responses aloud — click to mute" : "Speak agent responses aloud (active chat)",
127699
+ title: voiceTtsEnabled ? "Speaking agent responses aloud — click to mute (engine & voice in Settings)" : "Speak agent responses aloud (active chat) — engine & voice in Settings",
127587
127700
  className: voiceTtsEnabled ? "text-primary" : "",
127588
127701
  onClick: () => setVoiceTtsEnabled(!voiceTtsEnabled),
127589
127702
  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" })
127590
127703
  }),
127591
- voiceTts.available && voiceTtsEnabled && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
127592
- value: voiceTtsMode,
127593
- onChange: (e) => setVoiceTtsMode(e.target.value),
127594
- title: "Voice engine — Kokoro (server, natural) falls back to browser automatically",
127595
- className: "h-7 rounded border border-border bg-background px-1 text-xs",
127596
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
127597
- value: "kokoro",
127598
- children: "Kokoro"
127599
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
127600
- value: "browser",
127601
- children: "Browser"
127602
- })]
127603
- }), voiceTtsMode === "kokoro" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("select", {
127604
- value: voiceTtsKokoroVoice,
127605
- onChange: (e) => setVoiceTtsKokoroVoice(e.target.value),
127606
- title: "Kokoro voice",
127607
- className: "h-7 rounded border border-border bg-background px-1 text-xs",
127608
- children: [...KOKORO_VOICES, ...KOKORO_VOICES.some((v) => v.id === voiceTtsKokoroVoice) ? [] : [{
127609
- id: voiceTtsKokoroVoice,
127610
- label: voiceTtsKokoroVoice
127611
- }]].map((v) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
127612
- value: v.id,
127613
- children: v.label
127614
- }, v.id))
127615
- }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
127616
- value: voiceTtsLang,
127617
- onChange: (e) => setVoiceTtsLang(e.target.value),
127618
- title: "Voice language",
127619
- className: "h-7 rounded border border-border bg-background px-1 text-xs",
127620
- children: [[...new Set([
127621
- "en-US",
127622
- ...speechLangs,
127623
- ...voiceTtsLang ? [voiceTtsLang] : []
127624
- ])].sort().map((l) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
127625
- value: l,
127626
- children: l
127627
- }, l)), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
127628
- value: "",
127629
- children: "Browser default"
127630
- })]
127631
- })] }),
127632
127704
  canOpenTerminal && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
127633
127705
  variant: "ghost",
127634
127706
  size: "icon-sm",
@@ -154222,30 +154294,101 @@ function WorkspaceSettings() {
154222
154294
  });
154223
154295
  }
154224
154296
  function VoiceSettings() {
154297
+ const voiceTtsEnabled = useRelayStore((s) => s.voiceTtsEnabled);
154298
+ const setVoiceTtsEnabled = useRelayStore((s) => s.setVoiceTtsEnabled);
154299
+ const voiceTtsMode = useRelayStore((s) => s.voiceTtsMode);
154300
+ const setVoiceTtsMode = useRelayStore((s) => s.setVoiceTtsMode);
154301
+ const voiceTtsKokoroVoice = useRelayStore((s) => s.voiceTtsKokoroVoice);
154302
+ const setVoiceTtsKokoroVoice = useRelayStore((s) => s.setVoiceTtsKokoroVoice);
154303
+ const voiceTtsBrowserVoice = useRelayStore((s) => s.voiceTtsBrowserVoice);
154304
+ const setVoiceTtsBrowserVoice = useRelayStore((s) => s.setVoiceTtsBrowserVoice);
154225
154305
  const voiceInputMode = useRelayStore((s) => s.voiceInputMode);
154226
154306
  const setVoiceInputMode = useRelayStore((s) => s.setVoiceInputMode);
154307
+ const [browserVoices, setBrowserVoices] = (0, import_react.useState)(() => availableSpeechVoices());
154308
+ (0, import_react.useEffect)(() => {
154309
+ if (typeof window === "undefined" || !("speechSynthesis" in window)) return;
154310
+ const refresh = () => setBrowserVoices(availableSpeechVoices());
154311
+ refresh();
154312
+ window.speechSynthesis.addEventListener?.("voiceschanged", refresh);
154313
+ return () => window.speechSynthesis.removeEventListener?.("voiceschanged", refresh);
154314
+ }, []);
154315
+ const kokoroOptions = KOKORO_VOICES.some((v) => v.id === voiceTtsKokoroVoice) ? KOKORO_VOICES : [...KOKORO_VOICES, {
154316
+ id: voiceTtsKokoroVoice,
154317
+ label: voiceTtsKokoroVoice
154318
+ }];
154227
154319
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", {
154228
154320
  className: "space-y-3 rounded-lg border p-4",
154229
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", {
154230
- className: "text-sm font-semibold",
154231
- children: "Voice"
154232
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
154233
- className: "text-xs text-muted-foreground",
154234
- children: "How push-to-talk behaves in chat."
154235
- })] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, {
154236
- label: "Push-to-talk input",
154237
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Select$1, {
154238
- value: voiceInputMode,
154239
- onValueChange: (v) => setVoiceInputMode(v),
154240
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectTrigger, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectValue, {}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SelectContent, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, {
154241
- value: "compose",
154242
- children: "Fill the message box (review, then Enter)"
154243
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, {
154244
- value: "autosend",
154245
- children: "Send immediately (speak-and-send)"
154246
- })] })]
154321
+ children: [
154322
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
154323
+ className: "flex items-center justify-between gap-2",
154324
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
154325
+ className: "flex items-center gap-2",
154326
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Volume2, { className: "w-4 h-4" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", {
154327
+ className: "text-sm font-semibold",
154328
+ children: "Voice"
154329
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
154330
+ className: "text-xs text-muted-foreground",
154331
+ children: "Speak agent responses aloud in the active chat, and how push-to-talk behaves."
154332
+ })] })]
154333
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Switch, {
154334
+ checked: voiceTtsEnabled,
154335
+ onCheckedChange: setVoiceTtsEnabled
154336
+ })]
154337
+ }),
154338
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, {
154339
+ label: "Speech engine",
154340
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Select$1, {
154341
+ value: voiceTtsMode,
154342
+ onValueChange: (v) => setVoiceTtsMode(v),
154343
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectTrigger, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectValue, {}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SelectContent, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, {
154344
+ value: "kokoro",
154345
+ children: "Kokoro (server, natural — falls back to browser)"
154346
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, {
154347
+ value: "browser",
154348
+ children: "Browser (Web Speech API)"
154349
+ })] })]
154350
+ })
154351
+ }),
154352
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, {
154353
+ label: "Kokoro voice",
154354
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Select$1, {
154355
+ value: voiceTtsKokoroVoice,
154356
+ onValueChange: setVoiceTtsKokoroVoice,
154357
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectTrigger, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectValue, {}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectContent, { children: kokoroOptions.map((v) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, {
154358
+ value: v.id,
154359
+ children: v.label
154360
+ }, v.id)) })]
154361
+ })
154362
+ }),
154363
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, {
154364
+ label: "Browser voice",
154365
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Select$1, {
154366
+ value: voiceTtsBrowserVoice || "__default__",
154367
+ onValueChange: (v) => setVoiceTtsBrowserVoice(v === "__default__" ? "" : v),
154368
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectTrigger, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectValue, {}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SelectContent, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, {
154369
+ value: "__default__",
154370
+ children: "System default"
154371
+ }), browserVoices.map((v) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, {
154372
+ value: v.uri,
154373
+ children: v.label
154374
+ }, v.uri))] })]
154375
+ })
154376
+ }),
154377
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, {
154378
+ label: "Push-to-talk input",
154379
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Select$1, {
154380
+ value: voiceInputMode,
154381
+ onValueChange: (v) => setVoiceInputMode(v),
154382
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectTrigger, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectValue, {}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SelectContent, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, {
154383
+ value: "compose",
154384
+ children: "Fill the message box (review, then Enter)"
154385
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, {
154386
+ value: "autosend",
154387
+ children: "Send immediately (speak-and-send)"
154388
+ })] })]
154389
+ })
154247
154390
  })
154248
- })]
154391
+ ]
154249
154392
  });
154250
154393
  }
154251
154394
  var EFFORTS = [
@@ -159927,6 +160070,10 @@ if ("serviceWorker" in navigator) {
159927
160070
  min-width: calc(var(--spacing) * 0);
159928
160071
  }
159929
160072
 
160073
+ .min-w-4 {
160074
+ min-width: calc(var(--spacing) * 4);
160075
+ }
160076
+
159930
160077
  .min-w-5 {
159931
160078
  min-width: calc(var(--spacing) * 5);
159932
160079
  }
@@ -160981,6 +161128,16 @@ if ("serviceWorker" in navigator) {
160981
161128
  }
160982
161129
  }
160983
161130
 
161131
+ .bg-background\/95 {
161132
+ background-color: var(--background);
161133
+ }
161134
+
161135
+ @supports (color: color-mix(in lab, red, red)) {
161136
+ .bg-background\/95 {
161137
+ background-color: color-mix(in oklab, var(--background) 95%, transparent);
161138
+ }
161139
+ }
161140
+
160984
161141
  .bg-black\/0 {
160985
161142
  background-color: #0000;
160986
161143
  }
@@ -161851,10 +162008,6 @@ if ("serviceWorker" in navigator) {
161851
162008
  font-size: .8rem;
161852
162009
  }
161853
162010
 
161854
- .text-\[8px\] {
161855
- font-size: 8px;
161856
- }
161857
-
161858
162011
  .text-\[9px\] {
161859
162012
  font-size: 9px;
161860
162013
  }
@@ -163469,6 +163622,10 @@ if ("serviceWorker" in navigator) {
163469
163622
  }
163470
163623
 
163471
163624
  @media (min-width: 40rem) {
163625
+ .sm\:block {
163626
+ display: block;
163627
+ }
163628
+
163472
163629
  .sm\:flex {
163473
163630
  display: flex;
163474
163631
  }
@@ -84,6 +84,9 @@ export interface RunnerSpawnConfig {
84
84
  providerConfig: ProviderConfig;
85
85
  env: Record<string, string>;
86
86
  controlPort: number;
87
+ // Stage 2 (#215): the MCP endpoint the agent connects to — the runner-local proxy URL when the
88
+ // proxy is active. Undefined → the adapter targets the relay's MCP endpoint directly (Stage 1).
89
+ relayMcpEndpoint?: string;
87
90
  monitor?: {
88
91
  deliver(messages: Message[]): Promise<number[]>;
89
92
  };