agent-relay-server 0.21.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/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
  }
@@ -12099,9 +12146,11 @@ function isDashboardHidden() {
12099
12146
  function notificationPeer(notification) {
12100
12147
  return notification.threadPeer || notification.agentId || "";
12101
12148
  }
12149
+ function isActiveVisibleChat(peer, state) {
12150
+ return Boolean(peer && state.view === "chat" && state.selectedInboxThread === peer && !isDashboardHidden());
12151
+ }
12102
12152
  function notificationTargetsActiveChat(notification, state) {
12103
- const peer = notificationPeer(notification);
12104
- return Boolean(state.view === "chat" && peer && state.selectedInboxThread === peer && !isDashboardHidden());
12153
+ return isActiveVisibleChat(notificationPeer(notification), state);
12105
12154
  }
12106
12155
  function lastInboundMessageId(messages) {
12107
12156
  return messages.filter((m) => m.to === "user" && m.from !== "user").reduce((max, m) => Math.max(max, m.id), 0);
@@ -12174,6 +12223,7 @@ var useRelayStore = create$1()(persist((set, get) => ({
12174
12223
  voiceTtsLang: "en-US",
12175
12224
  voiceTtsMode: "kokoro",
12176
12225
  voiceTtsKokoroVoice: "am_michael",
12226
+ voiceTtsBrowserVoice: "",
12177
12227
  voiceInputMode: "compose",
12178
12228
  agentSort: "status",
12179
12229
  agentSortDir: "asc",
@@ -12218,6 +12268,8 @@ var useRelayStore = create$1()(persist((set, get) => ({
12218
12268
  chatAgentHostFilter: "",
12219
12269
  chatAgentSort: "status",
12220
12270
  chatAgentSortDir: "asc",
12271
+ chatAgentGroupBy: "",
12272
+ chatAgentFiltersCollapsed: true,
12221
12273
  chatStatusEvents: {},
12222
12274
  chatStickToBottom: true,
12223
12275
  chatHasNewItems: false,
@@ -12380,6 +12432,10 @@ var useRelayStore = create$1()(persist((set, get) => ({
12380
12432
  voiceTts.setKokoroVoice(voice);
12381
12433
  set({ voiceTtsKokoroVoice: voice });
12382
12434
  },
12435
+ setVoiceTtsBrowserVoice(uri) {
12436
+ voiceTts.setBrowserVoice(uri);
12437
+ set({ voiceTtsBrowserVoice: uri });
12438
+ },
12383
12439
  setVoiceInputMode(mode) {
12384
12440
  set({ voiceInputMode: mode });
12385
12441
  },
@@ -12396,6 +12452,7 @@ var useRelayStore = create$1()(persist((set, get) => ({
12396
12452
  voiceTts.setLang(get().voiceTtsLang);
12397
12453
  voiceTts.setMode(get().voiceTtsMode);
12398
12454
  voiceTts.setKokoroVoice(get().voiceTtsKokoroVoice);
12455
+ voiceTts.setBrowserVoice(get().voiceTtsBrowserVoice);
12399
12456
  syncVoiceActiveChat(get());
12400
12457
  setUnauthorizedHandler(() => {
12401
12458
  if (!get().authNeeded) set({
@@ -12835,7 +12892,15 @@ var useRelayStore = create$1()(persist((set, get) => ({
12835
12892
  if (s.view === "messages" && s.selectedAgent) path += "&for=" + encodeURIComponent(s.selectedAgent);
12836
12893
  if (s.view === "messages" && s.channelFilter) path += "&channel=" + encodeURIComponent(s.channelFilter);
12837
12894
  const messages = await api("GET", path);
12838
- set({ messages: mergeFetchedMessages(get().messages, messages) });
12895
+ const merged = mergeFetchedMessages(get().messages, messages);
12896
+ set({ messages: merged });
12897
+ const after = get();
12898
+ const peer = after.selectedInboxThread;
12899
+ if (isActiveVisibleChat(peer, after)) {
12900
+ let lastId = 0;
12901
+ for (const m of merged) if (m.id > lastId && inboxPeer(m) === peer && isHumanInboundMessage(m) && !isSessionActivityStep(m)) lastId = m.id;
12902
+ if (lastId) get().markInboxThreadReadTo(peer, lastId);
12903
+ }
12839
12904
  } catch {}
12840
12905
  },
12841
12906
  async fetchThreadHistory(peer) {
@@ -12936,7 +13001,7 @@ var useRelayStore = create$1()(persist((set, get) => ({
12936
13001
  if (msgs.length > 500) msgs.splice(0, msgs.length - 500);
12937
13002
  set({ messages: msgs });
12938
13003
  const peer = inboxPeer(msg);
12939
- if (isHumanInboundMessage(msg) && peer && s.view === "chat" && s.selectedInboxThread === peer && !isDashboardHidden()) get().markInboxThreadReadTo(peer, msg.id);
13004
+ if (isHumanInboundMessage(msg) && isActiveVisibleChat(peer, s)) get().markInboxThreadReadTo(peer, msg.id);
12940
13005
  return;
12941
13006
  }
12942
13007
  if (event === "message.queued" || event === "message.expired" || event === "message.delivery_updated" || event === "message.reaction_updated") {
@@ -14510,6 +14575,7 @@ var useRelayStore = create$1()(persist((set, get) => ({
14510
14575
  voiceTtsLang: state.voiceTtsLang,
14511
14576
  voiceTtsMode: state.voiceTtsMode,
14512
14577
  voiceTtsKokoroVoice: state.voiceTtsKokoroVoice,
14578
+ voiceTtsBrowserVoice: state.voiceTtsBrowserVoice,
14513
14579
  voiceInputMode: state.voiceInputMode,
14514
14580
  agentSort: state.agentSort,
14515
14581
  agentSortDir: state.agentSortDir,
@@ -14528,6 +14594,8 @@ var useRelayStore = create$1()(persist((set, get) => ({
14528
14594
  chatAgentHostFilter: state.chatAgentHostFilter,
14529
14595
  chatAgentSort: state.chatAgentSort,
14530
14596
  chatAgentSortDir: state.chatAgentSortDir,
14597
+ chatAgentGroupBy: state.chatAgentGroupBy,
14598
+ chatAgentFiltersCollapsed: state.chatAgentFiltersCollapsed,
14531
14599
  activityFilter: state.activityFilter,
14532
14600
  analyticsPeriod: state.analyticsPeriod,
14533
14601
  memoryFilters: state.memoryFilters,
@@ -99801,6 +99869,45 @@ function Button({ className, variant = "default", size = "default", asChild = fa
99801
99869
  });
99802
99870
  }
99803
99871
  //#endregion
99872
+ //#region src/components/shared/copy-button.tsx
99873
+ /**
99874
+ * Shared copy-to-clipboard button with a transient "copied" check state.
99875
+ * Consolidates the duplicated clipboard + timeout pattern used across views.
99876
+ */
99877
+ function CopyButton({ value, label = "Copy", copiedLabel = "Copied", showText = false, size, variant = "ghost", className, iconClassName, disabled, onCopied }) {
99878
+ const [copied, setCopied] = (0, import_react.useState)(false);
99879
+ const timer = (0, import_react.useRef)(null);
99880
+ (0, import_react.useEffect)(() => () => {
99881
+ if (timer.current) clearTimeout(timer.current);
99882
+ }, []);
99883
+ async function copy(e) {
99884
+ e.preventDefault();
99885
+ e.stopPropagation();
99886
+ try {
99887
+ await navigator.clipboard?.writeText(typeof value === "function" ? value() : value);
99888
+ setCopied(true);
99889
+ onCopied?.();
99890
+ if (timer.current) clearTimeout(timer.current);
99891
+ timer.current = setTimeout(() => setCopied(false), 1400);
99892
+ } catch {
99893
+ setCopied(false);
99894
+ }
99895
+ }
99896
+ const resolvedSize = size ?? (showText ? "sm" : "icon-sm");
99897
+ const iconCls = cn$2("h-3.5 w-3.5", iconClassName);
99898
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
99899
+ type: "button",
99900
+ size: resolvedSize,
99901
+ variant,
99902
+ className,
99903
+ disabled,
99904
+ title: copied ? copiedLabel : label,
99905
+ "aria-label": copied ? copiedLabel : label,
99906
+ onClick: (e) => void copy(e),
99907
+ children: [copied ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Check, { className: iconCls }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Copy, { className: iconCls }), showText && (copied ? copiedLabel : label)]
99908
+ });
99909
+ }
99910
+ //#endregion
99804
99911
  //#region \0vite/preload-helper.js
99805
99912
  var scriptRel, assetsURL, seen, __vitePreload;
99806
99913
  var init_preload_helper = __esmMin((() => {
@@ -108465,25 +108572,14 @@ var CodePreview = (0, import_react.memo)(function CodePreview({ content, path, m
108465
108572
  const [html, setHtml] = (0, import_react.useState)("");
108466
108573
  const [loading, setLoading] = (0, import_react.useState)(false);
108467
108574
  const [failed, setFailed] = (0, import_react.useState)(false);
108468
- const [copied, setCopied] = (0, import_react.useState)(false);
108469
- async function copyCode() {
108470
- try {
108471
- await navigator.clipboard?.writeText(content);
108472
- setCopied(true);
108473
- window.setTimeout(() => setCopied(false), 1400);
108474
- } catch {
108475
- setCopied(false);
108476
- }
108477
- }
108478
108575
  function copyButton() {
108479
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
108480
- type: "button",
108576
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CopyButton, {
108577
+ value: content,
108578
+ label: "Copy code",
108579
+ copiedLabel: "Copied code",
108481
108580
  size: "icon",
108482
108581
  variant: "ghost",
108483
- className: "absolute right-2 top-2 h-7 w-7 bg-background/80 opacity-0 shadow-sm backdrop-blur transition-opacity hover:bg-muted group-hover/code:opacity-100 focus-visible:opacity-100",
108484
- onClick: copyCode,
108485
- title: copied ? "Copied code" : "Copy code",
108486
- children: copied ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Check, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Copy, { className: "h-3.5 w-3.5" })
108582
+ className: "absolute right-2 top-2 h-7 w-7 bg-background/80 opacity-0 shadow-sm backdrop-blur transition-opacity hover:bg-muted group-hover/code:opacity-100 focus-visible:opacity-100"
108487
108583
  });
108488
108584
  }
108489
108585
  (0, import_react.useEffect)(() => {
@@ -108780,7 +108876,7 @@ function ContextMeter({ agent }) {
108780
108876
  function runtimeChips(agent) {
108781
108877
  const spawned = isRelaySpawned(agent);
108782
108878
  const approval = approvalMode(agent);
108783
- const workspace = workspaceMode(agent);
108879
+ const workspace = agentWorkspaceMeta(agent);
108784
108880
  return [
108785
108881
  {
108786
108882
  key: "origin",
@@ -108820,7 +108916,7 @@ function approvalMode(agent) {
108820
108916
  className: "border-amber-500/30 bg-amber-500/10 text-amber-300"
108821
108917
  };
108822
108918
  }
108823
- function workspaceMode(agent) {
108919
+ function agentWorkspaceMeta(agent) {
108824
108920
  const workspace = recordValue(agent.meta?.workspace);
108825
108921
  const requested = stringValue(workspace.requestedMode) || stringValue(agent.meta?.workspaceMode);
108826
108922
  const effective = stringValue(workspace.mode);
@@ -108830,6 +108926,7 @@ function workspaceMode(agent) {
108830
108926
  label: "Isolated",
108831
108927
  title: "Workspace: isolated worktree",
108832
108928
  Icon: GitBranch,
108929
+ iconColor: "text-violet-300",
108833
108930
  className: "border-violet-500/30 bg-violet-500/10 text-violet-300"
108834
108931
  };
108835
108932
  if (mode === "shared") return {
@@ -108837,6 +108934,7 @@ function workspaceMode(agent) {
108837
108934
  label: "Shared",
108838
108935
  title: "Workspace: shared repo checkout",
108839
108936
  Icon: Share2,
108937
+ iconColor: "text-cyan-300",
108840
108938
  className: "border-cyan-500/30 bg-cyan-500/10 text-cyan-300"
108841
108939
  };
108842
108940
  return {
@@ -108844,12 +108942,18 @@ function workspaceMode(agent) {
108844
108942
  label: "Default",
108845
108943
  title: effective ? `Workspace: default/inherit, resolved to ${effective}` : "Workspace: default/inherit",
108846
108944
  Icon: Layers,
108945
+ iconColor: "text-zinc-400",
108847
108946
  className: "border-zinc-500/30 bg-zinc-500/10 text-zinc-300"
108848
108947
  };
108849
108948
  }
108850
108949
  function recordValue(value) {
108851
108950
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
108852
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
+ }
108853
108957
  function runtimeBadges(agent) {
108854
108958
  const caps = agent.providerCapabilities;
108855
108959
  const context = agent.context;
@@ -108865,40 +108969,47 @@ function runtimeBadges(agent) {
108865
108969
  if (context) badges.push(`${Math.round(context.utilization * 100)}% ctx`);
108866
108970
  return badges;
108867
108971
  }
108868
- function ContextRing({ utilization, className }) {
108972
+ function ContextRing({ utilization, size = 26, className }) {
108869
108973
  const pct = Math.round(Math.min(1, Math.max(0, utilization)) * 100);
108870
- const r = 10;
108974
+ const stroke = size >= 24 ? 3 : 2.5;
108975
+ const c = size / 2;
108976
+ const r = c - stroke;
108871
108977
  const circumference = 2 * Math.PI * r;
108872
108978
  const offset = circumference * (1 - Math.min(1, Math.max(0, utilization)));
108873
108979
  const color = utilization > .8 ? "text-red-400" : utilization > .5 ? "text-yellow-400" : "text-emerald-400";
108874
108980
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
108875
108981
  className: `relative shrink-0 ${className ?? ""}`,
108982
+ style: {
108983
+ width: size,
108984
+ height: size
108985
+ },
108876
108986
  title: `Context: ${pct}%`,
108877
108987
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", {
108878
- width: "26",
108879
- height: "26",
108880
- viewBox: "0 0 26 26",
108988
+ width: size,
108989
+ height: size,
108990
+ viewBox: `0 0 ${size} ${size}`,
108881
108991
  className: "rotate-[-90deg]",
108882
108992
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", {
108883
- cx: "13",
108884
- cy: "13",
108993
+ cx: c,
108994
+ cy: c,
108885
108995
  r,
108886
108996
  fill: "none",
108887
- strokeWidth: "3",
108997
+ strokeWidth: stroke,
108888
108998
  className: "stroke-muted-foreground/20"
108889
108999
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", {
108890
- cx: "13",
108891
- cy: "13",
109000
+ cx: c,
109001
+ cy: c,
108892
109002
  r,
108893
109003
  fill: "none",
108894
- strokeWidth: "3",
109004
+ strokeWidth: stroke,
108895
109005
  strokeDasharray: circumference,
108896
109006
  strokeDashoffset: offset,
108897
109007
  strokeLinecap: "round",
108898
109008
  className: `${color} stroke-current transition-all duration-500`
108899
109009
  })]
108900
109010
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
108901
- 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 },
108902
109013
  children: pct
108903
109014
  })]
108904
109015
  });
@@ -124794,7 +124905,6 @@ function FileContent({ orchestratorId, selectedPath, line, onReadError }) {
124794
124905
  const { file, loading, error } = useFileRead(orchestratorId, selectedPath);
124795
124906
  const lineRef = (0, import_react.useRef)(null);
124796
124907
  const [mode, setMode] = (0, import_react.useState)("raw");
124797
- const [copiedPath, setCopiedPath] = (0, import_react.useState)(false);
124798
124908
  (0, import_react.useEffect)(() => {
124799
124909
  onReadError(error);
124800
124910
  }, [error, onReadError]);
@@ -124816,15 +124926,6 @@ function FileContent({ orchestratorId, selectedPath, line, onReadError }) {
124816
124926
  file?.content,
124817
124927
  line
124818
124928
  ]);
124819
- async function copyPath(path) {
124820
- try {
124821
- await navigator.clipboard?.writeText(path);
124822
- setCopiedPath(true);
124823
- window.setTimeout(() => setCopiedPath(false), 1400);
124824
- } catch {
124825
- setCopiedPath(false);
124826
- }
124827
- }
124828
124929
  function selectMode(nextMode, kind) {
124829
124930
  setMode(nextMode);
124830
124931
  if (kind) writeModePreference(kind, nextMode);
@@ -124849,14 +124950,13 @@ function FileContent({ orchestratorId, selectedPath, line, onReadError }) {
124849
124950
  className: "min-w-0 flex-1 truncate font-mono",
124850
124951
  children: file.path
124851
124952
  }),
124852
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
124853
- type: "button",
124953
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CopyButton, {
124954
+ value: file.path,
124955
+ label: "Copy path",
124956
+ copiedLabel: "Copied path",
124854
124957
  size: "icon",
124855
124958
  variant: "ghost",
124856
- className: "h-7 w-7 shrink-0",
124857
- onClick: () => copyPath(file.path),
124858
- title: copiedPath ? "Copied path" : "Copy path",
124859
- children: copiedPath ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Check, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Copy, { className: "h-3.5 w-3.5" })
124959
+ className: "h-7 w-7 shrink-0"
124860
124960
  }),
124861
124961
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge$1, {
124862
124962
  variant: "outline",
@@ -124913,14 +125013,13 @@ function FileContent({ orchestratorId, selectedPath, line, onReadError }) {
124913
125013
  className: "min-w-0 flex-1 truncate font-mono",
124914
125014
  children: file.path
124915
125015
  }),
124916
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
124917
- type: "button",
125016
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CopyButton, {
125017
+ value: file.path,
125018
+ label: "Copy path",
125019
+ copiedLabel: "Copied path",
124918
125020
  size: "icon",
124919
125021
  variant: "ghost",
124920
- className: "h-7 w-7 shrink-0",
124921
- onClick: () => copyPath(file.path),
124922
- title: copiedPath ? "Copied path" : "Copy path",
124923
- children: copiedPath ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Check, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Copy, { className: "h-3.5 w-3.5" })
125022
+ className: "h-7 w-7 shrink-0"
124924
125023
  }),
124925
125024
  file.truncated && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge$1, {
124926
125025
  variant: "secondary",
@@ -125617,6 +125716,8 @@ function AgentListPanel({ threads, onSelectAgent }) {
125617
125716
  const chatAgentTagFilter = useRelayStore((s) => s.chatAgentTagFilter);
125618
125717
  const chatAgentCapFilter = useRelayStore((s) => s.chatAgentCapFilter);
125619
125718
  const chatAgentHostFilter = useRelayStore((s) => s.chatAgentHostFilter);
125719
+ const chatAgentGroupBy = useRelayStore((s) => s.chatAgentGroupBy);
125720
+ const chatAgentFiltersCollapsed = useRelayStore((s) => s.chatAgentFiltersCollapsed);
125620
125721
  const now = useNow();
125621
125722
  const chatAgents = useChatAgents();
125622
125723
  const threadByPeer = (0, import_react.useMemo)(() => {
@@ -125640,161 +125741,287 @@ function AgentListPanel({ threads, onSelectAgent }) {
125640
125741
  const uniqueTags = (0, import_react.useMemo)(() => [...new Set(chatAgents.flatMap((a) => a.tags || []))], [chatAgents]);
125641
125742
  const uniqueCaps = (0, import_react.useMemo)(() => [...new Set(chatAgents.flatMap((a) => userFacingCapabilities(a.capabilities || [])))], [chatAgents]);
125642
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;
125643
125762
  function handleSelect(id) {
125644
125763
  openInboxThread(id, threadByPeer.get(id)?.messages);
125645
125764
  onSelectAgent?.(id);
125646
125765
  }
125647
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125648
- className: "flex flex-col h-full border-r border-border min-w-0",
125649
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125650
- className: "p-3 border-b border-border space-y-2",
125651
- children: [
125652
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125653
- className: "relative",
125654
- 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, {
125655
- placeholder: "Search agents...",
125656
- value: chatAgentSearch,
125657
- onChange: (e) => set({ chatAgentSearch: e.target.value }),
125658
- className: "pl-8 h-7 text-xs"
125659
- })]
125660
- }),
125661
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125662
- className: "flex items-center gap-1.5",
125663
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
125664
- value: chatAgentSort,
125665
- onChange: (e) => set({ chatAgentSort: e.target.value }),
125666
- 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",
125667
125807
  children: [
125668
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125669
- value: "status",
125670
- children: "By status"
125671
- }),
125672
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125673
- value: "name",
125674
- 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) })
125675
125812
  }),
125676
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125677
- value: "lastSeen",
125678
- children: "By last seen"
125813
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
125814
+ className: "truncate",
125815
+ children: project || ws.label
125679
125816
  }),
125680
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125681
- value: "lastMessage",
125682
- 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
+ })
125683
125823
  })
125684
125824
  ]
125685
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
125686
- variant: "ghost",
125687
- size: "icon-xs",
125688
- onClick: () => set({ chatAgentSortDir: chatAgentSortDir === "asc" ? "desc" : "asc" }),
125689
- 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" })
125690
- })]
125691
- }),
125692
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125693
- className: "flex gap-1",
125694
- children: [
125695
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
125696
- value: chatAgentProviderFilter,
125697
- onChange: (e) => set({ chatAgentProviderFilter: e.target.value }),
125698
- 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",
125699
125862
  children: [
125700
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125701
- value: "",
125702
- children: "All types"
125703
- }),
125704
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125705
- value: "claude",
125706
- children: "Claude"
125707
- }),
125708
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125709
- value: "codex",
125710
- children: "Codex"
125711
- }),
125712
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125713
- value: "agent",
125714
- children: "Agent"
125715
- }),
125716
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125717
- value: "user",
125718
- 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
125719
125868
  })
125720
125869
  ]
125721
- }),
125722
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
125723
- value: chatAgentStatusFilter,
125724
- onChange: (e) => set({ chatAgentStatusFilter: e.target.value }),
125725
- className: "flex-1 h-6 text-xs rounded-md border border-border bg-background px-1.5 text-foreground",
125726
- children: [
125727
- /* @__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", {
125728
125911
  value: "",
125729
- children: "All status"
125730
- }),
125731
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125732
- value: "online",
125733
- children: "Online"
125734
- }),
125735
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125736
- value: "idle",
125737
- children: "Idle"
125738
- }),
125739
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125740
- value: "busy",
125741
- children: "Busy"
125742
- }),
125743
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125744
- value: "offline",
125745
- children: "Offline"
125746
- })
125747
- ]
125748
- }),
125749
- uniqueHosts.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
125750
- value: chatAgentHostFilter,
125751
- onChange: (e) => set({ chatAgentHostFilter: e.target.value }),
125752
- className: "flex-1 h-6 text-xs rounded-md border border-border bg-background px-1.5 text-foreground",
125753
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125754
- value: "",
125755
- children: "All hosts"
125756
- }), uniqueHosts.map((h) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125757
- value: h,
125758
- children: h
125759
- }, h))]
125760
- })
125761
- ]
125762
- }),
125763
- (uniqueTags.length > 0 || uniqueCaps.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125764
- className: "flex gap-1",
125765
- children: [uniqueTags.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
125766
- value: chatAgentTagFilter,
125767
- onChange: (e) => set({ chatAgentTagFilter: e.target.value }),
125768
- className: "flex-1 h-6 text-xs rounded-md border border-border bg-background px-1.5 text-foreground",
125769
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125770
- value: "",
125771
- children: "All tags"
125772
- }), uniqueTags.map((t) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("option", {
125773
- value: t,
125774
- children: ["#", t]
125775
- }, t))]
125776
- }), uniqueCaps.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
125777
- value: chatAgentCapFilter,
125778
- onChange: (e) => set({ chatAgentCapFilter: e.target.value }),
125779
- className: "flex-1 h-6 text-xs rounded-md border border-border bg-background px-1.5 text-foreground",
125780
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125781
- value: "",
125782
- children: "All caps"
125783
- }), uniqueCaps.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
125784
- value: c,
125785
- children: c
125786
- }, c))]
125787
- })]
125788
- })
125789
- ]
125790
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125791
- className: "flex-1 overflow-y-auto",
125792
- 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", {
125793
126020
  type: "button",
125794
- 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",
125795
126022
  onClick: openAgentSpawn,
125796
126023
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
125797
- 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",
125798
126025
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Plus, { className: "h-3.5 w-3.5" })
125799
126026
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125800
126027
  className: "min-w-0 flex-1",
@@ -125802,71 +126029,31 @@ function AgentListPanel({ threads, onSelectAgent }) {
125802
126029
  className: "text-xs font-medium",
125803
126030
  children: "New Agent"
125804
126031
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
125805
- className: "mt-0.5 truncate text-xs text-muted-foreground",
126032
+ className: "truncate text-[10px] text-muted-foreground",
125806
126033
  children: "Spawn a fresh chat agent"
125807
126034
  })]
125808
126035
  })]
125809
- }), sortedAgents.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125810
- className: "flex flex-col items-center justify-center py-10 text-center px-4",
125811
- 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", {
125812
- className: "text-xs text-muted-foreground",
125813
- children: "No agents match filters"
125814
- })]
125815
- }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { children: sortedAgents.map((agent) => {
125816
- const thread = threadByPeer.get(agent.id);
125817
- const unread = thread?.attention.unread || 0;
125818
- const lastMsg = thread?.previewMessage;
125819
- const lastActivityAt = threadActivityTimestamp(thread);
125820
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
125821
- 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"),
125822
- onClick: () => handleSelect(agent.id),
125823
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125824
- className: "relative shrink-0 mt-0.5",
125825
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AgentTypeIcon, { agent }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusDot, {
125826
- agent,
125827
- now,
125828
- className: "absolute -bottom-0.5 -right-0.5 w-2 h-2"
125829
- })]
125830
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125831
- className: "flex-1 min-w-0",
125832
- children: [
125833
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125834
- className: "flex items-center justify-between gap-1 mb-0.5",
125835
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
125836
- className: "text-xs font-medium truncate",
125837
- children: displayName(agent)
125838
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125839
- className: "flex items-center gap-1 shrink-0",
125840
- children: [lastActivityAt > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
125841
- className: "text-[10px] text-muted-foreground",
125842
- title: fmtTime$1(lastActivityAt),
125843
- children: timeAgo(now, lastActivityAt)
125844
- }), unread > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge$1, {
125845
- className: "bg-red-500 text-white text-xs min-w-[18px] h-[18px] flex items-center justify-center shrink-0 px-1",
125846
- children: unread
125847
- })]
125848
- })]
125849
- }),
125850
- typeof agent.meta?.cwd === "string" && agent.meta.cwd && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
125851
- className: "font-mono text-[10px] text-muted-foreground/60 truncate",
125852
- children: shortPath$1(agent.meta.cwd)
125853
- }),
125854
- lastMsg && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
125855
- className: "text-xs text-muted-foreground truncate leading-tight",
125856
- children: [lastMsg.from === "user" ? "You: " : "", messagePreview(lastMsg)]
125857
- }),
125858
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
125859
- className: "flex flex-wrap gap-1 mt-1",
125860
- children: agent.status !== "offline" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
125861
- className: cn$2("text-xs", agent.status === "busy" ? "text-yellow-400" : agent.status === "idle" ? "text-emerald-400" : "text-emerald-400"),
125862
- children: agent.status
125863
- })
125864
- })
125865
- ]
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"
125866
126044
  })]
125867
- }) }, agent.id);
125868
- }) })]
125869
- })]
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
+ ]
125870
126057
  });
125871
126058
  }
125872
126059
  function chatTimestamp(iso) {
@@ -125926,40 +126113,6 @@ var TIMELINE_STATUS_LABELS = {
125926
126113
  var TIMELINE_STATUSES = new Set(Object.keys(TIMELINE_STATUS_LABELS));
125927
126114
  var STATUS_DEDUPE_WINDOW_MS = 3e3;
125928
126115
  var CHAT_BOTTOM_THRESHOLD_PX = 96;
125929
- var KOKORO_VOICES = [
125930
- {
125931
- id: "am_michael",
125932
- label: "Michael (US ♂)"
125933
- },
125934
- {
125935
- id: "am_adam",
125936
- label: "Adam (US ♂)"
125937
- },
125938
- {
125939
- id: "af_heart",
125940
- label: "Heart (US ♀)"
125941
- },
125942
- {
125943
- id: "af_bella",
125944
- label: "Bella (US ♀)"
125945
- },
125946
- {
125947
- id: "af_nicole",
125948
- label: "Nicole (US ♀)"
125949
- },
125950
- {
125951
- id: "af_sarah",
125952
- label: "Sarah (US ♀)"
125953
- },
125954
- {
125955
- id: "bm_george",
125956
- label: "George (UK ♂)"
125957
- },
125958
- {
125959
- id: "bf_emma",
125960
- label: "Emma (UK ♀)"
125961
- }
125962
- ];
125963
126116
  function StatusMarker({ event }) {
125964
126117
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
125965
126118
  className: "flex items-center justify-center gap-2 py-2 my-1",
@@ -127119,21 +127272,7 @@ function ChatPanel({ threads, onBack, showBackButton }) {
127119
127272
  const fetchOrchestrators = useRelayStore((s) => s.fetchOrchestrators);
127120
127273
  const voiceTtsEnabled = useRelayStore((s) => s.voiceTtsEnabled);
127121
127274
  const setVoiceTtsEnabled = useRelayStore((s) => s.setVoiceTtsEnabled);
127122
- const voiceTtsLang = useRelayStore((s) => s.voiceTtsLang);
127123
- const setVoiceTtsLang = useRelayStore((s) => s.setVoiceTtsLang);
127124
- const voiceTtsMode = useRelayStore((s) => s.voiceTtsMode);
127125
- const setVoiceTtsMode = useRelayStore((s) => s.setVoiceTtsMode);
127126
- const voiceTtsKokoroVoice = useRelayStore((s) => s.voiceTtsKokoroVoice);
127127
- const setVoiceTtsKokoroVoice = useRelayStore((s) => s.setVoiceTtsKokoroVoice);
127128
127275
  const voiceInputMode = useRelayStore((s) => s.voiceInputMode);
127129
- const [speechLangs, setSpeechLangs] = (0, import_react.useState)(() => availableSpeechLangs());
127130
- (0, import_react.useEffect)(() => {
127131
- if (!voiceTts.available) return;
127132
- const refresh = () => setSpeechLangs(availableSpeechLangs());
127133
- refresh();
127134
- window.speechSynthesis.addEventListener?.("voiceschanged", refresh);
127135
- return () => window.speechSynthesis.removeEventListener?.("voiceschanged", refresh);
127136
- }, []);
127137
127276
  const fileInputRef = (0, import_react.useRef)(null);
127138
127277
  const pttRecorderRef = (0, import_react.useRef)(null);
127139
127278
  const [micState, setMicState] = (0, import_react.useState)("idle");
@@ -127549,55 +127688,19 @@ function ChatPanel({ threads, onBack, showBackButton }) {
127549
127688
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
127550
127689
  className: "flex items-center gap-0.5 md:gap-1 shrink-0",
127551
127690
  children: agent && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
127691
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CopyButton, {
127692
+ value: agent.id,
127693
+ label: "Copy agent ID",
127694
+ size: "icon-sm"
127695
+ }),
127552
127696
  voiceTts.available && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
127553
127697
  variant: "ghost",
127554
127698
  size: "icon-sm",
127555
- 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",
127556
127700
  className: voiceTtsEnabled ? "text-primary" : "",
127557
127701
  onClick: () => setVoiceTtsEnabled(!voiceTtsEnabled),
127558
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" })
127559
127703
  }),
127560
- voiceTts.available && voiceTtsEnabled && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
127561
- value: voiceTtsMode,
127562
- onChange: (e) => setVoiceTtsMode(e.target.value),
127563
- title: "Voice engine — Kokoro (server, natural) falls back to browser automatically",
127564
- className: "h-7 rounded border border-border bg-background px-1 text-xs",
127565
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
127566
- value: "kokoro",
127567
- children: "Kokoro"
127568
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
127569
- value: "browser",
127570
- children: "Browser"
127571
- })]
127572
- }), voiceTtsMode === "kokoro" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("select", {
127573
- value: voiceTtsKokoroVoice,
127574
- onChange: (e) => setVoiceTtsKokoroVoice(e.target.value),
127575
- title: "Kokoro voice",
127576
- className: "h-7 rounded border border-border bg-background px-1 text-xs",
127577
- children: [...KOKORO_VOICES, ...KOKORO_VOICES.some((v) => v.id === voiceTtsKokoroVoice) ? [] : [{
127578
- id: voiceTtsKokoroVoice,
127579
- label: voiceTtsKokoroVoice
127580
- }]].map((v) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
127581
- value: v.id,
127582
- children: v.label
127583
- }, v.id))
127584
- }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
127585
- value: voiceTtsLang,
127586
- onChange: (e) => setVoiceTtsLang(e.target.value),
127587
- title: "Voice language",
127588
- className: "h-7 rounded border border-border bg-background px-1 text-xs",
127589
- children: [[...new Set([
127590
- "en-US",
127591
- ...speechLangs,
127592
- ...voiceTtsLang ? [voiceTtsLang] : []
127593
- ])].sort().map((l) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
127594
- value: l,
127595
- children: l
127596
- }, l)), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", {
127597
- value: "",
127598
- children: "Browser default"
127599
- })]
127600
- })] }),
127601
127704
  canOpenTerminal && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
127602
127705
  variant: "ghost",
127603
127706
  size: "icon-sm",
@@ -128193,6 +128296,13 @@ function AgentCard({ agent }) {
128193
128296
  className: "flex gap-1 mt-2.5 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity",
128194
128297
  onClick: (e) => e.stopPropagation(),
128195
128298
  children: [
128299
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CopyButton, {
128300
+ value: agent.id,
128301
+ label: "Copy agent ID",
128302
+ size: "icon",
128303
+ className: "h-7 w-7",
128304
+ iconClassName: "w-3 h-3"
128305
+ }),
128196
128306
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
128197
128307
  size: "icon",
128198
128308
  variant: "ghost",
@@ -129392,9 +129502,6 @@ function WorkspaceActions({ workspace, expanded, onToggleDetails }) {
129392
129502
  const gitState = useRelayStore((s) => s.workspaceGitState[workspace.id]);
129393
129503
  const landed = !!gitState && gitState.available !== false && gitState.landed === true;
129394
129504
  const mergeable = workspace.mode === "isolated" && Boolean(workspace.worktreePath) && MERGEABLE_STATUSES.has(workspace.status) && !landed;
129395
- async function copyPath() {
129396
- await navigator.clipboard?.writeText(openPath);
129397
- }
129398
129505
  async function merge() {
129399
129506
  await fetchWorkspaceMergePreview(workspace.id);
129400
129507
  await workspaceAction(workspace.id, "merge");
@@ -129417,13 +129524,13 @@ function WorkspaceActions({ workspace, expanded, onToggleDetails }) {
129417
129524
  onClick: () => void openFilesAt({ path: openPath }),
129418
129525
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FolderOpen, { className: "h-3.5 w-3.5" })
129419
129526
  }),
129420
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
129527
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CopyButton, {
129528
+ value: openPath,
129529
+ label: "Copy path",
129530
+ copiedLabel: "Copied path",
129421
129531
  size: "icon-sm",
129422
129532
  variant: "ghost",
129423
- title: "Copy path",
129424
- disabled: !openPath,
129425
- onClick: () => void copyPath(),
129426
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Copy, { className: "h-3.5 w-3.5" })
129533
+ disabled: !openPath
129427
129534
  }),
129428
129535
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
129429
129536
  size: "icon-sm",
@@ -130703,10 +130810,6 @@ function SecurityView() {
130703
130810
  await api("POST", `/tokens/${encodeURIComponent(token.jti)}/revoke`);
130704
130811
  await refresh();
130705
130812
  }
130706
- async function copy(value) {
130707
- await navigator.clipboard?.writeText(value);
130708
- setStatus("Copied");
130709
- }
130710
130813
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
130711
130814
  className: "space-y-4",
130712
130815
  children: [
@@ -130936,11 +131039,12 @@ function SecurityView() {
130936
131039
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
130937
131040
  className: "text-xs text-muted-foreground",
130938
131041
  children: "New token"
130939
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
130940
- variant: "ghost",
131042
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CopyButton, {
131043
+ value: issuedToken,
131044
+ label: "Copy token",
130941
131045
  size: "sm",
130942
- onClick: () => void copy(issuedToken),
130943
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Copy, { className: "h-3.5 w-3.5" })
131046
+ variant: "ghost",
131047
+ onCopied: () => setStatus("Copied")
130944
131048
  })]
130945
131049
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", {
130946
131050
  className: "block max-h-24 overflow-auto break-all text-xs",
@@ -153632,8 +153736,11 @@ function MaintenanceView() {
153632
153736
  })]
153633
153737
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ScrollArea, {
153634
153738
  className: "h-[calc(100dvh-10rem)]",
153635
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153636
- className: "overflow-x-auto rounded-md border border-border",
153739
+ children: jobs.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153740
+ className: "rounded-md border border-border px-3 py-12 text-center text-sm text-muted-foreground",
153741
+ children: "No maintenance jobs registered"
153742
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153743
+ className: "hidden overflow-x-auto rounded-md border border-border md:block",
153637
153744
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("table", {
153638
153745
  className: "w-full min-w-[980px] text-sm",
153639
153746
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("thead", {
@@ -153668,22 +153775,117 @@ function MaintenanceView() {
153668
153775
  children: "Action"
153669
153776
  })
153670
153777
  ] })
153671
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: jobs.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tr", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", {
153672
- colSpan: 7,
153673
- className: "px-3 py-12 text-center text-sm text-muted-foreground",
153674
- children: "No maintenance jobs registered"
153675
- }) }) : jobs.map((job) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MaintenanceRow, {
153778
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("tbody", { children: jobs.map((job) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MaintenanceRow, {
153676
153779
  job,
153677
153780
  now,
153678
153781
  onRun: () => void runMaintenanceJob(job.id)
153679
153782
  }, job.id)) })]
153680
153783
  })
153681
- })
153784
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153785
+ className: "space-y-3 md:hidden",
153786
+ children: jobs.map((job) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MaintenanceCard, {
153787
+ job,
153788
+ now,
153789
+ onRun: () => void runMaintenanceJob(job.id)
153790
+ }, job.id))
153791
+ })] })
153682
153792
  })]
153683
153793
  });
153684
153794
  }
153795
+ function jobStatus(job) {
153796
+ return job.running ? "running" : job.enabled ? job.lastStatus : "disabled";
153797
+ }
153798
+ function StatusBadge({ status }) {
153799
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Badge$1, {
153800
+ variant: "outline",
153801
+ className: cn$2("border", STATUS_CLASS[status] || STATUS_CLASS.idle),
153802
+ children: [statusIcon(status), status]
153803
+ });
153804
+ }
153805
+ function MaintenanceCard({ job, now, onRun }) {
153806
+ const status = jobStatus(job);
153807
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
153808
+ className: "rounded-md border border-border p-3",
153809
+ children: [
153810
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
153811
+ className: "flex items-start justify-between gap-2",
153812
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
153813
+ className: "min-w-0",
153814
+ children: [
153815
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153816
+ className: "font-medium",
153817
+ children: job.title
153818
+ }),
153819
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153820
+ className: "mt-0.5 text-xs text-muted-foreground",
153821
+ children: job.description
153822
+ }),
153823
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153824
+ className: "mt-1 font-mono text-[11px] text-muted-foreground/80",
153825
+ children: job.id
153826
+ })
153827
+ ]
153828
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusBadge, { status })]
153829
+ }),
153830
+ job.consecutiveFailures > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
153831
+ className: "mt-2 text-xs text-red-400",
153832
+ children: [
153833
+ job.consecutiveFailures,
153834
+ " failure",
153835
+ job.consecutiveFailures === 1 ? "" : "s"
153836
+ ]
153837
+ }),
153838
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("dl", {
153839
+ className: "mt-3 grid grid-cols-3 gap-2 text-xs",
153840
+ children: [
153841
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
153842
+ className: "text-muted-foreground",
153843
+ children: "Last run"
153844
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", { children: job.lastRunAt ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
153845
+ title: fmtTime$1(job.lastRunAt),
153846
+ children: timeAgo(now, job.lastRunAt)
153847
+ }) : "never" })] }),
153848
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
153849
+ className: "text-muted-foreground",
153850
+ children: "Next run"
153851
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", { children: job.nextRunAt ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
153852
+ title: fmtTime$1(job.nextRunAt),
153853
+ children: nextRunText(now, job.nextRunAt)
153854
+ }) : "-" })] }),
153855
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
153856
+ className: "text-muted-foreground",
153857
+ children: "Duration"
153858
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", { children: job.lastDurationMs !== void 0 ? `${job.lastDurationMs}ms` : "-" })] })
153859
+ ]
153860
+ }),
153861
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
153862
+ className: "mt-3",
153863
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153864
+ className: "text-xs text-muted-foreground",
153865
+ children: "Result"
153866
+ }), job.lastError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153867
+ className: "mt-0.5 text-xs text-red-400 line-clamp-3",
153868
+ children: job.lastError
153869
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153870
+ className: "mt-0.5 font-mono text-[11px] text-muted-foreground line-clamp-3",
153871
+ children: resultSummary(job)
153872
+ })]
153873
+ }),
153874
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
153875
+ className: "mt-3 flex justify-end",
153876
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
153877
+ size: "sm",
153878
+ variant: "outline",
153879
+ disabled: !job.enabled || job.running,
153880
+ onClick: onRun,
153881
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Play, { className: "h-3.5 w-3.5" }), " Run"]
153882
+ })
153883
+ })
153884
+ ]
153885
+ });
153886
+ }
153685
153887
  function MaintenanceRow({ job, now, onRun }) {
153686
- const status = job.running ? "running" : job.enabled ? job.lastStatus : "disabled";
153888
+ const status = jobStatus(job);
153687
153889
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", {
153688
153890
  className: "border-t border-border align-top",
153689
153891
  children: [
@@ -153706,11 +153908,7 @@ function MaintenanceRow({ job, now, onRun }) {
153706
153908
  }),
153707
153909
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("td", {
153708
153910
  className: "px-3 py-3",
153709
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Badge$1, {
153710
- variant: "outline",
153711
- className: cn$2("border", STATUS_CLASS[status] || STATUS_CLASS.idle),
153712
- children: [statusIcon(status), status]
153713
- }), job.consecutiveFailures > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
153911
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusBadge, { status }), job.consecutiveFailures > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
153714
153912
  className: "mt-1 text-xs text-red-400",
153715
153913
  children: [
153716
153914
  job.consecutiveFailures,
@@ -154096,30 +154294,101 @@ function WorkspaceSettings() {
154096
154294
  });
154097
154295
  }
154098
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);
154099
154305
  const voiceInputMode = useRelayStore((s) => s.voiceInputMode);
154100
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
+ }];
154101
154319
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", {
154102
154320
  className: "space-y-3 rounded-lg border p-4",
154103
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", {
154104
- className: "text-sm font-semibold",
154105
- children: "Voice"
154106
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
154107
- className: "text-xs text-muted-foreground",
154108
- children: "How push-to-talk behaves in chat."
154109
- })] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Field, {
154110
- label: "Push-to-talk input",
154111
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Select$1, {
154112
- value: voiceInputMode,
154113
- onValueChange: (v) => setVoiceInputMode(v),
154114
- 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, {
154115
- value: "compose",
154116
- children: "Fill the message box (review, then Enter)"
154117
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SelectItem, {
154118
- value: "autosend",
154119
- children: "Send immediately (speak-and-send)"
154120
- })] })]
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
+ })
154121
154390
  })
154122
- })]
154391
+ ]
154123
154392
  });
154124
154393
  }
154125
154394
  var EFFORTS = [
@@ -154392,7 +154661,6 @@ function AgentDiagnostics({ agent, orchestrators }) {
154392
154661
  const now = useNow();
154393
154662
  const [policyHealth, setPolicyHealth] = (0, import_react.useState)(null);
154394
154663
  const [agentEvents, setAgentEvents] = (0, import_react.useState)([]);
154395
- const [copied, setCopied] = (0, import_react.useState)(false);
154396
154664
  const [expandedSections, setExpandedSections] = (0, import_react.useState)({
154397
154665
  spawn: true,
154398
154666
  workspace: true,
@@ -154436,7 +154704,7 @@ function AgentDiagnostics({ agent, orchestrators }) {
154436
154704
  [key]: !s[key]
154437
154705
  }));
154438
154706
  }
154439
- async function copyDiagnosticBundle() {
154707
+ function buildDiagnosticBundle() {
154440
154708
  const policy = policyHealth?.policy;
154441
154709
  const state = policyHealth?.state;
154442
154710
  const lines = [
@@ -154513,11 +154781,7 @@ function AgentDiagnostics({ agent, orchestrators }) {
154513
154781
  }
154514
154782
  const contracts = agent.meta?.contracts;
154515
154783
  if (contracts) for (const [k, v] of Object.entries(contracts)) lines.push(`Contract ${k}: ${v}`);
154516
- try {
154517
- await navigator.clipboard.writeText(lines.join("\n"));
154518
- setCopied(true);
154519
- setTimeout(() => setCopied(false), 2e3);
154520
- } catch {}
154784
+ return lines.join("\n");
154521
154785
  }
154522
154786
  const policy = policyHealth?.policy;
154523
154787
  const state = policyHealth?.state;
@@ -154529,12 +154793,14 @@ function AgentDiagnostics({ agent, orchestrators }) {
154529
154793
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h3", {
154530
154794
  className: "text-sm font-medium flex items-center gap-1.5",
154531
154795
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Stethoscope, { className: "w-3.5 h-3.5 text-muted-foreground" }), "Diagnostics"]
154532
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
154796
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CopyButton, {
154797
+ value: buildDiagnosticBundle,
154798
+ label: "Copy Bundle",
154799
+ showText: true,
154533
154800
  size: "sm",
154534
154801
  variant: "outline",
154535
154802
  className: "h-7 text-xs gap-1",
154536
- onClick: copyDiagnosticBundle,
154537
- children: [copied ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Check, { className: "w-3 h-3" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Copy, { className: "w-3 h-3" }), copied ? "Copied" : "Copy Bundle"]
154803
+ iconClassName: "w-3 h-3"
154538
154804
  })]
154539
154805
  }),
154540
154806
  policy && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CollapsibleSection, {
@@ -155727,13 +155993,21 @@ function AgentDetailDrawer() {
155727
155993
  className: "space-y-1 text-sm",
155728
155994
  children: [
155729
155995
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
155730
- className: "flex justify-between gap-2 min-w-0",
155996
+ className: "flex items-center justify-between gap-2 min-w-0",
155731
155997
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
155732
155998
  className: "text-muted-foreground shrink-0",
155733
155999
  children: "ID"
155734
- }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
155735
- className: "font-mono text-xs truncate",
155736
- children: agent.id
156000
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
156001
+ className: "flex min-w-0 items-center gap-1",
156002
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
156003
+ className: "font-mono text-xs truncate",
156004
+ children: agent.id
156005
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CopyButton, {
156006
+ value: agent.id,
156007
+ label: "Copy agent ID",
156008
+ size: "icon-xs",
156009
+ className: "shrink-0 text-muted-foreground"
156010
+ })]
155737
156011
  })]
155738
156012
  }),
155739
156013
  runtimePackage && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
@@ -157317,11 +157591,6 @@ function OrchestratorInstallModal() {
157317
157591
  setLoading(false);
157318
157592
  }
157319
157593
  }
157320
- async function copy() {
157321
- if (!command) return;
157322
- await navigator.clipboard.writeText(command);
157323
- showNotification("Install command copied");
157324
- }
157325
157594
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Dialog, {
157326
157595
  open,
157327
157596
  onOpenChange: (o) => !o && set({ orchestratorInstallOpen: false }),
@@ -157390,11 +157659,14 @@ function OrchestratorInstallModal() {
157390
157659
  onClick: () => set({ orchestratorInstallOpen: false }),
157391
157660
  children: "Close"
157392
157661
  }),
157393
- command && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
157662
+ command && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CopyButton, {
157663
+ value: command,
157664
+ label: "Copy",
157665
+ showText: true,
157666
+ size: "default",
157394
157667
  variant: "outline",
157395
157668
  className: "gap-1",
157396
- onClick: copy,
157397
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Copy, { className: "w-3.5 h-3.5" }), "Copy"]
157669
+ onCopied: () => showNotification("Install command copied")
157398
157670
  }),
157399
157671
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
157400
157672
  className: "gap-1",
@@ -159322,6 +159594,10 @@ if ("serviceWorker" in navigator) {
159322
159594
  display: inline-flex;
159323
159595
  }
159324
159596
 
159597
+ .table {
159598
+ display: table;
159599
+ }
159600
+
159325
159601
  .field-sizing-content {
159326
159602
  field-sizing: content;
159327
159603
  }
@@ -159794,6 +160070,10 @@ if ("serviceWorker" in navigator) {
159794
160070
  min-width: calc(var(--spacing) * 0);
159795
160071
  }
159796
160072
 
160073
+ .min-w-4 {
160074
+ min-width: calc(var(--spacing) * 4);
160075
+ }
160076
+
159797
160077
  .min-w-5 {
159798
160078
  min-width: calc(var(--spacing) * 5);
159799
160079
  }
@@ -160848,6 +161128,16 @@ if ("serviceWorker" in navigator) {
160848
161128
  }
160849
161129
  }
160850
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
+
160851
161141
  .bg-black\/0 {
160852
161142
  background-color: #0000;
160853
161143
  }
@@ -161718,10 +162008,6 @@ if ("serviceWorker" in navigator) {
161718
162008
  font-size: .8rem;
161719
162009
  }
161720
162010
 
161721
- .text-\[8px\] {
161722
- font-size: 8px;
161723
- }
161724
-
161725
162011
  .text-\[9px\] {
161726
162012
  font-size: 9px;
161727
162013
  }
@@ -163336,6 +163622,10 @@ if ("serviceWorker" in navigator) {
163336
163622
  }
163337
163623
 
163338
163624
  @media (min-width: 40rem) {
163625
+ .sm\:block {
163626
+ display: block;
163627
+ }
163628
+
163339
163629
  .sm\:flex {
163340
163630
  display: flex;
163341
163631
  }