@burdenoff/microfe-vibecontrols 2026.625.2 → 2026.625.3

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.
@@ -44,10 +44,17 @@ var o = {
44
44
  return n === e ? t : n;
45
45
  }, h = o[c], g = !!d || !!f, _ = e(c, u), v = [];
46
46
  _.phase === "none" ? u && (c === "DEGRADED" || c === "OFFLINE") && v.push(`${h.label}: ${u}`) : (v.push(`${_.label} — ${_.actionHint}`), u && v.push(`Details: ${u}`)), g && v.push(m("agents.statusBadge.connectionLogHint", s));
47
- let y = v.length > 0 ? v.join(" · ") : void 0, b = !!h.spinner || _.inProgress, x = `inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium border transition-colors duration-150 ${h.color} ${l}`, S = g ? " cursor-pointer hover:opacity-90 transition-transform duration-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-strong" : "", C = /* @__PURE__ */ a(r, { children: [b && /* @__PURE__ */ i(n, {
48
- className: "size-3 animate-spin",
49
- "aria-hidden": "true"
50
- }), h.label] });
47
+ let y = v.length > 0 ? v.join(" · ") : void 0, b = !!h.spinner || _.inProgress, x = `inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium border transition-colors duration-150 ${h.color} ${l}`, S = g ? " cursor-pointer hover:opacity-90 transition-transform duration-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-strong" : "", C = /* @__PURE__ */ a(r, { children: [
48
+ b && /* @__PURE__ */ i(n, {
49
+ className: "size-3 animate-spin",
50
+ "aria-hidden": "true"
51
+ }),
52
+ c === "ACTIVE" && !b && /* @__PURE__ */ i("span", {
53
+ "aria-hidden": "true",
54
+ className: "size-1.5 rounded-full bg-current motion-safe:animate-vc-glow-pulse"
55
+ }),
56
+ h.label
57
+ ] });
51
58
  return g ? /* @__PURE__ */ i("a", {
52
59
  href: d ?? "#",
53
60
  title: y,
@@ -1 +1 @@
1
- {"version":3,"file":"AgentStatusBadge.js","names":[],"sources":["../../../src/components/agents/AgentStatusBadge.tsx"],"sourcesContent":["import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Loader2 } from 'lucide-react';\nimport type { FC, MouseEvent as ReactMouseEvent } from 'react';\nimport type { AgentStatus } from './types';\nimport { classifyAgentHealth } from '../../utils/agentHealth';\n\nexport interface AgentStatusBadgeProps {\n status: AgentStatus;\n className?: string;\n /**\n * Optional probe error to expose as a hover tooltip (and aria-label).\n * Surfaces the actual reason a `DEGRADED` agent is unhealthy — most\n * commonly \"boot state awaiting-config\" when the gateway-auth push\n * during onboarding silently failed. Without this, users see a yellow\n * badge with no signal about WHY, and have to curl /health/ready\n * themselves to debug.\n */\n reason?: string | null;\n /**\n * Optional href that turns the badge into an anchor pointing at the\n * agent's Connection Log tab. When this is set the badge is keyboard-\n * focusable, supports Ctrl/Cmd-click to open in a new tab, and surfaces\n * a \"click to see connection log\" tooltip suffix so users know it's\n * actionable. Provide together with `onClick` if you also need a SPA\n * navigation handler — the anchor `href` will still be the source of\n * truth for the browser's new-tab affordance.\n */\n linkTo?: string;\n /**\n * SPA navigation callback fired on unmodified left-click. When the\n * caller is already on the agent's detail page this is typically a\n * `setActiveTab('connection')` call; when the caller is on a list view\n * it's typically a `navigate(detailUrl + '?tab=connection')` call. The\n * anchor still owns the modifier-key path so Ctrl-click opens the\n * destination in a new tab regardless.\n */\n onClick?: (event: ReactMouseEvent<HTMLAnchorElement>) => void;\n}\n\n// Subtle-recipe pills: a tinted background + same-hue saturated text + a\n// matching border. This passes WCAG AA in BOTH light and dark themes (unlike\n// the old solid `bg-status-X-bg` + `text-status-X-text`, which was same-hue\n// low-contrast). Transient states (registering/connecting) read NEUTRAL with a\n// spinner instead of off-brand blue — the spinner is the \"working\" signal.\nconst statusConfig: Record<AgentStatus, { label: string; color: string; spinner?: boolean }> = {\n REGISTERING: {\n label: 'Registering',\n color: 'bg-bg-sunken text-text-secondary border-border-default',\n spinner: true,\n },\n CONNECTING: {\n label: 'Connecting',\n color: 'bg-bg-sunken text-text-secondary border-border-default',\n spinner: true,\n },\n ACTIVE: {\n label: 'Active',\n color: 'bg-status-success-bg-subtle text-status-success-text border-status-success-border',\n },\n STOPPED: {\n label: 'Stopped',\n color: 'bg-status-warning-bg-subtle text-status-warning-text border-status-warning-border',\n },\n IDLE: {\n label: 'Idle',\n color: 'bg-status-warning-bg-subtle text-status-warning-text border-status-warning-border',\n },\n DEGRADED: {\n label: 'Degraded',\n color: 'bg-status-warning-bg-subtle text-status-warning-text border-status-warning-border',\n },\n OFFLINE: {\n label: 'Offline',\n color: 'bg-bg-sunken text-text-secondary border-border-default',\n },\n DELETED: {\n label: 'Deleted',\n color: 'bg-status-error-bg-subtle text-status-error-text border-status-error-border',\n },\n};\n\nconst CONNECTION_LOG_HINT = 'See connection log for details';\n\n/**\n * AgentStatusBadge - Display agent status with appropriate styling.\n *\n * When `linkTo` or `onClick` is supplied the badge renders as an anchor\n * and becomes the canonical entry point into the agent's Connection Log\n * tab. This keeps the user one click away from the timeline of probe /\n * register / gateway-auth-push events any time they spot a non-Active\n * badge.\n */\nexport const AgentStatusBadge: FC<AgentStatusBadgeProps> = ({\n status,\n className = '',\n reason,\n linkTo,\n onClick,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string) => {\n const v = t(key);\n return v === key ? fallback : v;\n };\n const config = statusConfig[status];\n const isInteractive = Boolean(linkTo) || Boolean(onClick);\n\n // Classify a degraded/offline agent so the badge can say whether work is\n // happening behind the scenes (spinner, \"no action needed\") or the user has\n // an action item — rather than just a yellow badge with a raw health string.\n const health = classifyAgentHealth(status, reason);\n\n const tooltipParts: string[] = [];\n if (health.phase !== 'none') {\n tooltipParts.push(`${health.label} — ${health.actionHint}`);\n if (reason) tooltipParts.push(`Details: ${reason}`);\n } else if (reason && (status === 'DEGRADED' || status === 'OFFLINE')) {\n tooltipParts.push(`${config.label}: ${reason}`);\n }\n if (isInteractive) {\n tooltipParts.push(tr('agents.statusBadge.connectionLogHint', CONNECTION_LOG_HINT));\n }\n const tooltip = tooltipParts.length > 0 ? tooltipParts.join(' · ') : undefined;\n\n // Show the in-progress spinner for CONNECTING and for a DEGRADED agent that\n // is still provisioning, so \"something is happening\" is visible at a glance.\n const showSpinner = Boolean(config.spinner) || health.inProgress;\n\n const baseClassName = `inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium border transition-colors duration-150 ${config.color} ${className}`;\n const interactiveClassName = isInteractive\n ? ' cursor-pointer hover:opacity-90 transition-transform duration-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-strong'\n : '';\n\n const content = (\n <>\n {showSpinner && <Loader2 className=\"size-3 animate-spin\" aria-hidden=\"true\" />}\n {config.label}\n </>\n );\n\n if (isInteractive) {\n return (\n <a\n href={linkTo ?? '#'}\n title={tooltip}\n aria-label={tooltip ?? config.label}\n className={baseClassName + interactiveClassName}\n onClick={(event) => {\n // Let modifier-clicks reach the browser's default tab-opener so\n // Ctrl/Cmd-click + middle-click open the connection log in a\n // new tab. SPA navigation only fires on plain left-click.\n if (event.metaKey || event.ctrlKey || event.shiftKey || event.button !== 0) {\n return;\n }\n if (onClick) {\n onClick(event);\n }\n }}\n >\n {content}\n </a>\n );\n }\n\n return (\n <span title={tooltip} aria-label={tooltip ?? config.label} className={baseClassName}>\n {content}\n </span>\n );\n};\n"],"mappings":";;;;;AA4CA,IAAM,IAAyF;CAC7F,aAAa;EACX,OAAO;EACP,OAAO;EACP,SAAS;EACV;CACD,YAAY;EACV,OAAO;EACP,OAAO;EACP,SAAS;EACV;CACD,QAAQ;EACN,OAAO;EACP,OAAO;EACR;CACD,SAAS;EACP,OAAO;EACP,OAAO;EACR;CACD,MAAM;EACJ,OAAO;EACP,OAAO;EACR;CACD,UAAU;EACR,OAAO;EACP,OAAO;EACR;CACD,SAAS;EACP,OAAO;EACP,OAAO;EACR;CACD,SAAS;EACP,OAAO;EACP,OAAO;EACR;CACF,EAEK,IAAsB,kCAWf,KAA+C,EAC1D,WACA,eAAY,IACZ,WACA,WACA,iBACI;CACJ,IAAM,EAAE,SAAM,GAAS,EACjB,KAAM,GAAa,MAAqB;EAC5C,IAAM,IAAI,EAAE,EAAI;AAChB,SAAO,MAAM,IAAM,IAAW;IAE1B,IAAS,EAAa,IACtB,IAAgB,EAAQ,KAAW,EAAQ,GAK3C,IAAS,EAAoB,GAAQ,EAAO,EAE5C,IAAyB,EAAE;AAOjC,CANI,EAAO,UAAU,SAGV,MAAW,MAAW,cAAc,MAAW,cACxD,EAAa,KAAK,GAAG,EAAO,MAAM,IAAI,IAAS,IAH/C,EAAa,KAAK,GAAG,EAAO,MAAM,KAAK,EAAO,aAAa,EACvD,KAAQ,EAAa,KAAK,YAAY,IAAS,GAIjD,KACF,EAAa,KAAK,EAAG,wCAAwC,EAAoB,CAAC;CAEpF,IAAM,IAAU,EAAa,SAAS,IAAI,EAAa,KAAK,MAAM,GAAG,KAAA,GAI/D,IAAc,EAAQ,EAAO,WAAY,EAAO,YAEhD,IAAgB,uHAAuH,EAAO,MAAM,GAAG,KACvJ,IAAuB,IACzB,wJACA,IAEE,IACJ,kBAAA,GAAA,EAAA,UAAA,CACG,KAAe,kBAAC,GAAD;EAAS,WAAU;EAAsB,eAAY;EAAS,CAAA,EAC7E,EAAO,MACP,EAAA,CAAA;AA2BL,QAxBI,IAEA,kBAAC,KAAD;EACE,MAAM,KAAU;EAChB,OAAO;EACP,cAAY,KAAW,EAAO;EAC9B,WAAW,IAAgB;EAC3B,UAAU,MAAU;AAId,KAAM,WAAW,EAAM,WAAW,EAAM,YAAY,EAAM,WAAW,KAGrE,KACF,EAAQ,EAAM;;YAIjB;EACC,CAAA,GAKN,kBAAC,QAAD;EAAM,OAAO;EAAS,cAAY,KAAW,EAAO;EAAO,WAAW;YACnE;EACI,CAAA"}
1
+ {"version":3,"file":"AgentStatusBadge.js","names":[],"sources":["../../../src/components/agents/AgentStatusBadge.tsx"],"sourcesContent":["import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Loader2 } from 'lucide-react';\nimport type { FC, MouseEvent as ReactMouseEvent } from 'react';\nimport type { AgentStatus } from './types';\nimport { classifyAgentHealth } from '../../utils/agentHealth';\n\nexport interface AgentStatusBadgeProps {\n status: AgentStatus;\n className?: string;\n /**\n * Optional probe error to expose as a hover tooltip (and aria-label).\n * Surfaces the actual reason a `DEGRADED` agent is unhealthy — most\n * commonly \"boot state awaiting-config\" when the gateway-auth push\n * during onboarding silently failed. Without this, users see a yellow\n * badge with no signal about WHY, and have to curl /health/ready\n * themselves to debug.\n */\n reason?: string | null;\n /**\n * Optional href that turns the badge into an anchor pointing at the\n * agent's Connection Log tab. When this is set the badge is keyboard-\n * focusable, supports Ctrl/Cmd-click to open in a new tab, and surfaces\n * a \"click to see connection log\" tooltip suffix so users know it's\n * actionable. Provide together with `onClick` if you also need a SPA\n * navigation handler — the anchor `href` will still be the source of\n * truth for the browser's new-tab affordance.\n */\n linkTo?: string;\n /**\n * SPA navigation callback fired on unmodified left-click. When the\n * caller is already on the agent's detail page this is typically a\n * `setActiveTab('connection')` call; when the caller is on a list view\n * it's typically a `navigate(detailUrl + '?tab=connection')` call. The\n * anchor still owns the modifier-key path so Ctrl-click opens the\n * destination in a new tab regardless.\n */\n onClick?: (event: ReactMouseEvent<HTMLAnchorElement>) => void;\n}\n\n// Subtle-recipe pills: a tinted background + same-hue saturated text + a\n// matching border. This passes WCAG AA in BOTH light and dark themes (unlike\n// the old solid `bg-status-X-bg` + `text-status-X-text`, which was same-hue\n// low-contrast). Transient states (registering/connecting) read NEUTRAL with a\n// spinner instead of off-brand blue — the spinner is the \"working\" signal.\nconst statusConfig: Record<AgentStatus, { label: string; color: string; spinner?: boolean }> = {\n REGISTERING: {\n label: 'Registering',\n color: 'bg-bg-sunken text-text-secondary border-border-default',\n spinner: true,\n },\n CONNECTING: {\n label: 'Connecting',\n color: 'bg-bg-sunken text-text-secondary border-border-default',\n spinner: true,\n },\n ACTIVE: {\n label: 'Active',\n color: 'bg-status-success-bg-subtle text-status-success-text border-status-success-border',\n },\n STOPPED: {\n label: 'Stopped',\n color: 'bg-status-warning-bg-subtle text-status-warning-text border-status-warning-border',\n },\n IDLE: {\n label: 'Idle',\n color: 'bg-status-warning-bg-subtle text-status-warning-text border-status-warning-border',\n },\n DEGRADED: {\n label: 'Degraded',\n color: 'bg-status-warning-bg-subtle text-status-warning-text border-status-warning-border',\n },\n OFFLINE: {\n label: 'Offline',\n color: 'bg-bg-sunken text-text-secondary border-border-default',\n },\n DELETED: {\n label: 'Deleted',\n color: 'bg-status-error-bg-subtle text-status-error-text border-status-error-border',\n },\n};\n\nconst CONNECTION_LOG_HINT = 'See connection log for details';\n\n/**\n * AgentStatusBadge - Display agent status with appropriate styling.\n *\n * When `linkTo` or `onClick` is supplied the badge renders as an anchor\n * and becomes the canonical entry point into the agent's Connection Log\n * tab. This keeps the user one click away from the timeline of probe /\n * register / gateway-auth-push events any time they spot a non-Active\n * badge.\n */\nexport const AgentStatusBadge: FC<AgentStatusBadgeProps> = ({\n status,\n className = '',\n reason,\n linkTo,\n onClick,\n}) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string) => {\n const v = t(key);\n return v === key ? fallback : v;\n };\n const config = statusConfig[status];\n const isInteractive = Boolean(linkTo) || Boolean(onClick);\n\n // Classify a degraded/offline agent so the badge can say whether work is\n // happening behind the scenes (spinner, \"no action needed\") or the user has\n // an action item — rather than just a yellow badge with a raw health string.\n const health = classifyAgentHealth(status, reason);\n\n const tooltipParts: string[] = [];\n if (health.phase !== 'none') {\n tooltipParts.push(`${health.label} — ${health.actionHint}`);\n if (reason) tooltipParts.push(`Details: ${reason}`);\n } else if (reason && (status === 'DEGRADED' || status === 'OFFLINE')) {\n tooltipParts.push(`${config.label}: ${reason}`);\n }\n if (isInteractive) {\n tooltipParts.push(tr('agents.statusBadge.connectionLogHint', CONNECTION_LOG_HINT));\n }\n const tooltip = tooltipParts.length > 0 ? tooltipParts.join(' · ') : undefined;\n\n // Show the in-progress spinner for CONNECTING and for a DEGRADED agent that\n // is still provisioning, so \"something is happening\" is visible at a glance.\n const showSpinner = Boolean(config.spinner) || health.inProgress;\n\n const baseClassName = `inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium border transition-colors duration-150 ${config.color} ${className}`;\n const interactiveClassName = isInteractive\n ? ' cursor-pointer hover:opacity-90 transition-transform duration-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-strong'\n : '';\n\n // Boff UI 2.0 — an ACTIVE agent gets a calm reduced-motion-safe glow-pulse\n // dot (opacity only, no movement) so \"live\" reads at a glance.\n const showLiveDot = status === 'ACTIVE' && !showSpinner;\n\n const content = (\n <>\n {showSpinner && <Loader2 className=\"size-3 animate-spin\" aria-hidden=\"true\" />}\n {showLiveDot && (\n <span\n aria-hidden=\"true\"\n className=\"size-1.5 rounded-full bg-current motion-safe:animate-vc-glow-pulse\"\n />\n )}\n {config.label}\n </>\n );\n\n if (isInteractive) {\n return (\n <a\n href={linkTo ?? '#'}\n title={tooltip}\n aria-label={tooltip ?? config.label}\n className={baseClassName + interactiveClassName}\n onClick={(event) => {\n // Let modifier-clicks reach the browser's default tab-opener so\n // Ctrl/Cmd-click + middle-click open the connection log in a\n // new tab. SPA navigation only fires on plain left-click.\n if (event.metaKey || event.ctrlKey || event.shiftKey || event.button !== 0) {\n return;\n }\n if (onClick) {\n onClick(event);\n }\n }}\n >\n {content}\n </a>\n );\n }\n\n return (\n <span title={tooltip} aria-label={tooltip ?? config.label} className={baseClassName}>\n {content}\n </span>\n );\n};\n"],"mappings":";;;;;AA4CA,IAAM,IAAyF;CAC7F,aAAa;EACX,OAAO;EACP,OAAO;EACP,SAAS;EACV;CACD,YAAY;EACV,OAAO;EACP,OAAO;EACP,SAAS;EACV;CACD,QAAQ;EACN,OAAO;EACP,OAAO;EACR;CACD,SAAS;EACP,OAAO;EACP,OAAO;EACR;CACD,MAAM;EACJ,OAAO;EACP,OAAO;EACR;CACD,UAAU;EACR,OAAO;EACP,OAAO;EACR;CACD,SAAS;EACP,OAAO;EACP,OAAO;EACR;CACD,SAAS;EACP,OAAO;EACP,OAAO;EACR;CACF,EAEK,IAAsB,kCAWf,KAA+C,EAC1D,WACA,eAAY,IACZ,WACA,WACA,iBACI;CACJ,IAAM,EAAE,SAAM,GAAS,EACjB,KAAM,GAAa,MAAqB;EAC5C,IAAM,IAAI,EAAE,EAAI;AAChB,SAAO,MAAM,IAAM,IAAW;IAE1B,IAAS,EAAa,IACtB,IAAgB,EAAQ,KAAW,EAAQ,GAK3C,IAAS,EAAoB,GAAQ,EAAO,EAE5C,IAAyB,EAAE;AAOjC,CANI,EAAO,UAAU,SAGV,MAAW,MAAW,cAAc,MAAW,cACxD,EAAa,KAAK,GAAG,EAAO,MAAM,IAAI,IAAS,IAH/C,EAAa,KAAK,GAAG,EAAO,MAAM,KAAK,EAAO,aAAa,EACvD,KAAQ,EAAa,KAAK,YAAY,IAAS,GAIjD,KACF,EAAa,KAAK,EAAG,wCAAwC,EAAoB,CAAC;CAEpF,IAAM,IAAU,EAAa,SAAS,IAAI,EAAa,KAAK,MAAM,GAAG,KAAA,GAI/D,IAAc,EAAQ,EAAO,WAAY,EAAO,YAEhD,IAAgB,uHAAuH,EAAO,MAAM,GAAG,KACvJ,IAAuB,IACzB,wJACA,IAME,IACJ,kBAAA,GAAA,EAAA,UAAA;EACG,KAAe,kBAAC,GAAD;GAAS,WAAU;GAAsB,eAAY;GAAS,CAAA;EAJ9D,MAAW,YAAY,CAAC,KAMtC,kBAAC,QAAD;GACE,eAAY;GACZ,WAAU;GACV,CAAA;EAEH,EAAO;EACP,EAAA,CAAA;AA2BL,QAxBI,IAEA,kBAAC,KAAD;EACE,MAAM,KAAU;EAChB,OAAO;EACP,cAAY,KAAW,EAAO;EAC9B,WAAW,IAAgB;EAC3B,UAAU,MAAU;AAId,KAAM,WAAW,EAAM,WAAW,EAAM,YAAY,EAAM,WAAW,KAGrE,KACF,EAAQ,EAAM;;YAIjB;EACC,CAAA,GAKN,kBAAC,QAAD;EAAM,OAAO;EAAS,cAAY,KAAW,EAAO;EAAO,WAAW;YACnE;EACI,CAAA"}
@@ -1,7 +1,7 @@
1
1
  import { useI18n as e } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
2
- import { jsx as t } from "react/jsx-runtime";
2
+ import { jsx as t, jsxs as n } from "react/jsx-runtime";
3
3
  //#region src/components/sessions/SessionStatusBadge.tsx
4
- var n = {
4
+ var r = {
5
5
  PENDING: {
6
6
  label: "Pending",
7
7
  color: "bg-bg-sunken text-text-secondary border-border-subtle"
@@ -30,14 +30,17 @@ var n = {
30
30
  label: "Terminated",
31
31
  color: "bg-bg-sunken text-text-secondary border-border-subtle"
32
32
  }
33
- }, r = ({ status: r, className: i = "" }) => {
34
- let { t: a } = e(), o = n[r];
35
- return /* @__PURE__ */ t("span", {
36
- className: `inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border transition-colors duration-150 ${o.color} ${i}`,
37
- children: o.label
33
+ }, i = ({ status: i, className: a = "" }) => {
34
+ let { t: o } = e(), s = r[i], c = i === "RUNNING";
35
+ return /* @__PURE__ */ n("span", {
36
+ className: `inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium border transition-colors duration-150 ${s.color} ${a}`,
37
+ children: [c && /* @__PURE__ */ t("span", {
38
+ "aria-hidden": "true",
39
+ className: "size-1.5 rounded-full bg-current motion-safe:animate-vc-glow-pulse"
40
+ }), s.label]
38
41
  });
39
42
  };
40
43
  //#endregion
41
- export { r as SessionStatusBadge };
44
+ export { i as SessionStatusBadge };
42
45
 
43
46
  //# sourceMappingURL=SessionStatusBadge.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"SessionStatusBadge.js","names":[],"sources":["../../../src/components/sessions/SessionStatusBadge.tsx"],"sourcesContent":["import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport type { FC } from 'react';\nimport type { SessionStatus } from './types';\n\nexport interface SessionStatusBadgeProps {\n status: SessionStatus;\n className?: string;\n}\n\n// Subtle-recipe pills (tinted background + same-hue saturated text + matching\n// border) so they pass WCAG AA in every theme and stay visually consistent with\n// the shared StatusBadge. Transient states (pending/starting) read NEUTRAL —\n// loud blue clashes with the purple brand accent.\nconst statusConfig: Record<SessionStatus, { label: string; color: string }> = {\n PENDING: {\n label: 'Pending',\n color: 'bg-bg-sunken text-text-secondary border-border-subtle',\n },\n STARTING: {\n label: 'Starting',\n color: 'bg-bg-sunken text-text-secondary border-border-subtle',\n },\n RUNNING: {\n label: 'Running',\n color: 'bg-status-success-bg-subtle text-status-success-text border-status-success-border',\n },\n STOPPING: {\n label: 'Stopping',\n color: 'bg-status-warning-bg-subtle text-status-warning-text border-status-warning-border',\n },\n STOPPED: {\n label: 'Stopped',\n color: 'bg-bg-sunken text-text-secondary border-border-subtle',\n },\n ERROR: {\n label: 'Error',\n color: 'bg-status-error-bg-subtle text-status-error-text border-status-error-border',\n },\n TERMINATED: {\n label: 'Terminated',\n color: 'bg-bg-sunken text-text-secondary border-border-subtle',\n },\n};\n\nexport const SessionStatusBadge: FC<SessionStatusBadgeProps> = ({ status, className = '' }) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string) => {\n const v = t(key);\n return v === key ? fallback : v;\n };\n const config = statusConfig[status];\n\n return (\n <span\n className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border transition-colors duration-150 ${config.color} ${className}`}\n >\n {config.label}\n </span>\n );\n};\n"],"mappings":";;;AAaA,IAAM,IAAwE;CAC5E,SAAS;EACP,OAAO;EACP,OAAO;EACR;CACD,UAAU;EACR,OAAO;EACP,OAAO;EACR;CACD,SAAS;EACP,OAAO;EACP,OAAO;EACR;CACD,UAAU;EACR,OAAO;EACP,OAAO;EACR;CACD,SAAS;EACP,OAAO;EACP,OAAO;EACR;CACD,OAAO;EACL,OAAO;EACP,OAAO;EACR;CACD,YAAY;EACV,OAAO;EACP,OAAO;EACR;CACF,EAEY,KAAmD,EAAE,WAAQ,eAAY,SAAS;CAC7F,IAAM,EAAE,SAAM,GAAS,EAKjB,IAAS,EAAa;AAE5B,QACE,kBAAC,QAAD;EACE,WAAW,iHAAiH,EAAO,MAAM,GAAG;YAE3I,EAAO;EACH,CAAA"}
1
+ {"version":3,"file":"SessionStatusBadge.js","names":[],"sources":["../../../src/components/sessions/SessionStatusBadge.tsx"],"sourcesContent":["import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport type { FC } from 'react';\nimport type { SessionStatus } from './types';\n\nexport interface SessionStatusBadgeProps {\n status: SessionStatus;\n className?: string;\n}\n\n// Subtle-recipe pills (tinted background + same-hue saturated text + matching\n// border) so they pass WCAG AA in every theme and stay visually consistent with\n// the shared StatusBadge. Transient states (pending/starting) read NEUTRAL —\n// loud blue clashes with the purple brand accent.\nconst statusConfig: Record<SessionStatus, { label: string; color: string }> = {\n PENDING: {\n label: 'Pending',\n color: 'bg-bg-sunken text-text-secondary border-border-subtle',\n },\n STARTING: {\n label: 'Starting',\n color: 'bg-bg-sunken text-text-secondary border-border-subtle',\n },\n RUNNING: {\n label: 'Running',\n color: 'bg-status-success-bg-subtle text-status-success-text border-status-success-border',\n },\n STOPPING: {\n label: 'Stopping',\n color: 'bg-status-warning-bg-subtle text-status-warning-text border-status-warning-border',\n },\n STOPPED: {\n label: 'Stopped',\n color: 'bg-bg-sunken text-text-secondary border-border-subtle',\n },\n ERROR: {\n label: 'Error',\n color: 'bg-status-error-bg-subtle text-status-error-text border-status-error-border',\n },\n TERMINATED: {\n label: 'Terminated',\n color: 'bg-bg-sunken text-text-secondary border-border-subtle',\n },\n};\n\nexport const SessionStatusBadge: FC<SessionStatusBadgeProps> = ({ status, className = '' }) => {\n const { t } = useI18n();\n const tr = (key: string, fallback: string) => {\n const v = t(key);\n return v === key ? fallback : v;\n };\n const config = statusConfig[status];\n\n // Boff UI 2.0 — a RUNNING session gets a calm reduced-motion-safe glow-pulse\n // dot (opacity only, no movement) so \"live\" reads at a glance.\n const showLiveDot = status === 'RUNNING';\n\n return (\n <span\n className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium border transition-colors duration-150 ${config.color} ${className}`}\n >\n {showLiveDot && (\n <span\n aria-hidden=\"true\"\n className=\"size-1.5 rounded-full bg-current motion-safe:animate-vc-glow-pulse\"\n />\n )}\n {config.label}\n </span>\n );\n};\n"],"mappings":";;;AAaA,IAAM,IAAwE;CAC5E,SAAS;EACP,OAAO;EACP,OAAO;EACR;CACD,UAAU;EACR,OAAO;EACP,OAAO;EACR;CACD,SAAS;EACP,OAAO;EACP,OAAO;EACR;CACD,UAAU;EACR,OAAO;EACP,OAAO;EACR;CACD,SAAS;EACP,OAAO;EACP,OAAO;EACR;CACD,OAAO;EACL,OAAO;EACP,OAAO;EACR;CACD,YAAY;EACV,OAAO;EACP,OAAO;EACR;CACF,EAEY,KAAmD,EAAE,WAAQ,eAAY,SAAS;CAC7F,IAAM,EAAE,SAAM,GAAS,EAKjB,IAAS,EAAa,IAItB,IAAc,MAAW;AAE/B,QACE,kBAAC,QAAD;EACE,WAAW,uHAAuH,EAAO,MAAM,GAAG;YADpJ,CAGG,KACC,kBAAC,QAAD;GACE,eAAY;GACZ,WAAU;GACV,CAAA,EAEH,EAAO,MACH"}
@@ -1,15 +1,21 @@
1
1
  import { jsx as e } from "react/jsx-runtime";
2
- import { EmptyState as t } from "@burdenoff/fe-libs/ui";
2
+ import { EmptyState as t, IllustratedEmptyState as n } from "@burdenoff/fe-libs/ui";
3
3
  //#region src/components/shared/EmptyState.tsx
4
- function n({ icon: n, title: r, description: i, action: a }) {
5
- return /* @__PURE__ */ e(t, {
6
- icon: n,
7
- title: r,
8
- description: i,
9
- action: a
4
+ function r({ icon: r, title: i, description: a, action: o, illustration: s, illustrationAlt: c }) {
5
+ return s ? /* @__PURE__ */ e(n, {
6
+ illustration: s,
7
+ alt: c ?? i,
8
+ title: i,
9
+ description: a,
10
+ action: o
11
+ }) : /* @__PURE__ */ e(t, {
12
+ icon: r,
13
+ title: i,
14
+ description: a,
15
+ action: o
10
16
  });
11
17
  }
12
18
  //#endregion
13
- export { n as EmptyState };
19
+ export { r as EmptyState };
14
20
 
15
21
  //# sourceMappingURL=EmptyState.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"EmptyState.js","names":[],"sources":["../../../src/components/shared/EmptyState.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { EmptyState as CanonicalEmptyState } from '@burdenoff/fe-libs/ui';\n\ninterface EmptyStateProps {\n icon?: ReactNode;\n title: string;\n description?: string;\n action?: ReactNode;\n}\n\n/**\n * Local wrapper around `EmptyState` from `@burdenoff/fe-libs/ui`.\n *\n * Keeps the existing microfe-vibecontrols call sites stable while routing all\n * empty-state rendering through the canonical primitive.\n */\nexport function EmptyState({ icon, title, description, action }: EmptyStateProps) {\n return (\n <CanonicalEmptyState icon={icon} title={title} description={description} action={action} />\n );\n}\n"],"mappings":";;;AAgBA,SAAgB,EAAW,EAAE,SAAM,UAAO,gBAAa,aAA2B;AAChF,QACE,kBAAC,GAAD;EAA2B;EAAa;EAAoB;EAAqB;EAAU,CAAA"}
1
+ {"version":3,"file":"EmptyState.js","names":[],"sources":["../../../src/components/shared/EmptyState.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport {\n EmptyState as CanonicalEmptyState,\n IllustratedEmptyState,\n} from '@burdenoff/fe-libs/ui';\n\ninterface EmptyStateProps {\n icon?: ReactNode;\n title: string;\n description?: string;\n action?: ReactNode;\n /**\n * Boff UI 2.0 — when set, the empty state renders the on-brand illustration\n * (served by the host shell at `/illustrations/<name>.webp`) instead of the\n * plain icon. Falls back to the icon path when omitted so existing call sites\n * stay visually unchanged.\n */\n illustration?: string;\n /** Optional alt text for the illustration (defaults to the title). */\n illustrationAlt?: string;\n}\n\n/**\n * Local wrapper around `EmptyState` from `@burdenoff/fe-libs/ui`.\n *\n * Keeps the existing microfe-vibecontrols call sites stable while routing all\n * empty-state rendering through the canonical primitive. When an `illustration`\n * is supplied it routes through the richer `IllustratedEmptyState` primitive.\n */\nexport function EmptyState({\n icon,\n title,\n description,\n action,\n illustration,\n illustrationAlt,\n}: EmptyStateProps) {\n if (illustration) {\n return (\n <IllustratedEmptyState\n illustration={illustration}\n alt={illustrationAlt ?? title}\n title={title}\n description={description}\n action={action}\n />\n );\n }\n return (\n <CanonicalEmptyState icon={icon} title={title} description={description} action={action} />\n );\n}\n"],"mappings":";;;AA6BA,SAAgB,EAAW,EACzB,SACA,UACA,gBACA,WACA,iBACA,sBACkB;AAYlB,QAXI,IAEA,kBAAC,GAAD;EACgB;EACd,KAAK,KAAmB;EACjB;EACM;EACL;EACR,CAAA,GAIJ,kBAAC,GAAD;EAA2B;EAAa;EAAoB;EAAqB;EAAU,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"PageHeader.js","names":[],"sources":["../../../src/components/shared/PageHeader.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { PageHeader as CanonicalPageHeader } from '@burdenoff/fe-libs/chrome';\n\ninterface PageHeaderProps {\n title: string;\n description?: string;\n actions?: ReactNode;\n breadcrumb?: { label: string; path?: string }[];\n}\n\n/**\n * Local wrapper around `PageHeader` from `@burdenoff/fe-libs/chrome`.\n *\n * Maps the legacy microfe-local `breadcrumb` shape (`{ label, path }`) onto\n * the canonical shape (`{ label, to }`). Adds the page-tour data attribute\n * that vibecontrols's onboarding tours target.\n */\nexport function PageHeader({ title, description, actions, breadcrumb }: PageHeaderProps) {\n return (\n <div data-tour=\"page-header\">\n <CanonicalPageHeader\n title={title}\n description={description}\n actions={actions ? <div data-tour=\"page-header-actions\">{actions}</div> : undefined}\n breadcrumb={breadcrumb?.map((b) => ({ label: b.label, to: b.path }))}\n />\n </div>\n );\n}\n"],"mappings":";;;AAiBA,SAAgB,EAAW,EAAE,UAAO,gBAAa,YAAS,iBAA+B;AACvF,QACE,kBAAC,OAAD;EAAK,aAAU;YACb,kBAAC,GAAD;GACS;GACM;GACb,SAAS,IAAU,kBAAC,OAAD;IAAK,aAAU;cAAuB;IAAc,CAAA,GAAG,KAAA;GAC1E,YAAY,GAAY,KAAK,OAAO;IAAE,OAAO,EAAE;IAAO,IAAI,EAAE;IAAM,EAAE;GACpE,CAAA;EACE,CAAA"}
1
+ {"version":3,"file":"PageHeader.js","names":[],"sources":["../../../src/components/shared/PageHeader.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { PageHeader as CanonicalPageHeader } from '@burdenoff/fe-libs/chrome';\n\ninterface PageHeaderProps {\n title: ReactNode;\n description?: string;\n actions?: ReactNode;\n breadcrumb?: { label: string; path?: string }[];\n}\n\n/**\n * Local wrapper around `PageHeader` from `@burdenoff/fe-libs/chrome`.\n *\n * Maps the legacy microfe-local `breadcrumb` shape (`{ label, path }`) onto\n * the canonical shape (`{ label, to }`). Adds the page-tour data attribute\n * that vibecontrols's onboarding tours target.\n */\nexport function PageHeader({ title, description, actions, breadcrumb }: PageHeaderProps) {\n return (\n <div data-tour=\"page-header\">\n <CanonicalPageHeader\n title={title}\n description={description}\n actions={actions ? <div data-tour=\"page-header-actions\">{actions}</div> : undefined}\n breadcrumb={breadcrumb?.map((b) => ({ label: b.label, to: b.path }))}\n />\n </div>\n );\n}\n"],"mappings":";;;AAiBA,SAAgB,EAAW,EAAE,UAAO,gBAAa,YAAS,iBAA+B;AACvF,QACE,kBAAC,OAAD;EAAK,aAAU;YACb,kBAAC,GAAD;GACS;GACM;GACb,SAAS,IAAU,kBAAC,OAAD;IAAK,aAAU;cAAuB;IAAc,CAAA,GAAG,KAAA;GAC1E,YAAY,GAAY,KAAK,OAAO;IAAE,OAAO,EAAE;IAAO,IAAI,EAAE;IAAM,EAAE;GACpE,CAAA;EACE,CAAA"}
@@ -1,7 +1,10 @@
1
1
  import { jsx as e, jsxs as t } from "react/jsx-runtime";
2
2
  //#region src/components/shared/PageSkeleton.tsx
3
3
  function n({ width: t = "w-full", height: n = "h-4", className: r = "" }) {
4
- return /* @__PURE__ */ e("div", { className: `animate-pulse rounded bg-bg-sunken ${t} ${n} ${r}` });
4
+ return /* @__PURE__ */ e("div", {
5
+ className: `relative overflow-hidden rounded bg-bg-sunken ${t} ${n} ${r}`,
6
+ children: /* @__PURE__ */ e("div", { className: "absolute inset-0 -translate-x-full bg-[linear-gradient(90deg,transparent,color-mix(in_oklab,var(--color-text-primary)_8%,transparent),transparent)] motion-safe:animate-vc-shimmer" })
7
+ });
5
8
  }
6
9
  function r({ count: r = 6, rowClassName: i = "" }) {
7
10
  return /* @__PURE__ */ e("div", {
@@ -1 +1 @@
1
- {"version":3,"file":"PageSkeleton.js","names":[],"sources":["../../../src/components/shared/PageSkeleton.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\n\ninterface SkeletonLineProps {\n width?: string;\n height?: string;\n className?: string;\n}\n\nexport function SkeletonLine({\n width = 'w-full',\n height = 'h-4',\n className = '',\n}: SkeletonLineProps) {\n return <div className={`animate-pulse rounded bg-bg-sunken ${width} ${height} ${className}`} />;\n}\n\ninterface SkeletonRowsProps {\n count?: number;\n rowClassName?: string;\n}\n\nexport function SkeletonRows({ count = 6, rowClassName = '' }: SkeletonRowsProps) {\n return (\n <div className=\"space-y-3\" aria-hidden=\"true\">\n {Array.from({ length: count }).map((_, i) => (\n <div\n key={i}\n className={`flex items-center gap-3 rounded-card border border-border-subtle bg-bg-surface shadow-elevation-1 p-4 ${rowClassName}`}\n >\n <SkeletonLine width=\"w-8\" height=\"h-8\" className=\"rounded-full\" />\n <div className=\"flex-1 space-y-2\">\n <SkeletonLine width=\"w-1/3\" />\n <SkeletonLine width=\"w-1/2\" height=\"h-3\" />\n </div>\n <SkeletonLine width=\"w-16\" height=\"h-6\" />\n </div>\n ))}\n </div>\n );\n}\n\ninterface SkeletonCardsProps {\n count?: number;\n columns?: number;\n}\n\nexport function SkeletonCards({ count = 6, columns = 3 }: SkeletonCardsProps) {\n const gridCols =\n columns === 4\n ? 'sm:grid-cols-2 lg:grid-cols-4'\n : columns === 2\n ? 'sm:grid-cols-2'\n : 'sm:grid-cols-2 lg:grid-cols-3';\n return (\n <div className={`grid grid-cols-1 gap-4 ${gridCols}`} aria-hidden=\"true\">\n {Array.from({ length: count }).map((_, i) => (\n <div\n key={i}\n className=\"rounded-card border border-border-subtle bg-bg-surface shadow-elevation-1 p-5 space-y-3\"\n >\n <SkeletonLine width=\"w-2/3\" height=\"h-5\" />\n <SkeletonLine width=\"w-full\" height=\"h-3\" />\n <SkeletonLine width=\"w-1/2\" height=\"h-3\" />\n </div>\n ))}\n </div>\n );\n}\n\ninterface PageSkeletonProps {\n header?: ReactNode;\n variant?: 'rows' | 'cards' | 'detail';\n count?: number;\n}\n\nexport function PageSkeleton({ header, variant = 'rows', count = 6 }: PageSkeletonProps) {\n return (\n <div className=\"space-y-6\" role=\"status\" aria-label=\"Loading content\">\n {header}\n {variant === 'cards' ? (\n <SkeletonCards count={count} />\n ) : variant === 'detail' ? (\n <div className=\"space-y-3\">\n <SkeletonLine width=\"w-1/3\" height=\"h-6\" />\n <SkeletonLine width=\"w-2/3\" height=\"h-3\" />\n <SkeletonLine width=\"w-full\" height=\"h-3\" />\n <SkeletonLine width=\"w-3/4\" height=\"h-3\" />\n <SkeletonRows count={Math.max(count - 2, 3)} />\n </div>\n ) : (\n <SkeletonRows count={count} />\n )}\n </div>\n );\n}\n"],"mappings":";;AAQA,SAAgB,EAAa,EAC3B,WAAQ,UACR,YAAS,OACT,eAAY,MACQ;AACpB,QAAO,kBAAC,OAAD,EAAK,WAAW,sCAAsC,EAAM,GAAG,EAAO,GAAG,KAAe,CAAA;;AAQjG,SAAgB,EAAa,EAAE,WAAQ,GAAG,kBAAe,MAAyB;AAChF,QACE,kBAAC,OAAD;EAAK,WAAU;EAAY,eAAY;YACpC,MAAM,KAAK,EAAE,QAAQ,GAAO,CAAC,CAAC,KAAK,GAAG,MACrC,kBAAC,OAAD;GAEE,WAAW,yGAAyG;aAFtH;IAIE,kBAAC,GAAD;KAAc,OAAM;KAAM,QAAO;KAAM,WAAU;KAAiB,CAAA;IAClE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAc,OAAM,SAAU,CAAA,EAC9B,kBAAC,GAAD;MAAc,OAAM;MAAQ,QAAO;MAAQ,CAAA,CACvC;;IACN,kBAAC,GAAD;KAAc,OAAM;KAAO,QAAO;KAAQ,CAAA;IACtC;KATC,EASD,CACN;EACE,CAAA;;AASV,SAAgB,EAAc,EAAE,WAAQ,GAAG,aAAU,KAAyB;AAO5E,QACE,kBAAC,OAAD;EAAK,WAAW,0BANhB,MAAY,IACR,kCACA,MAAY,IACV,mBACA;EAEgD,eAAY;YAC/D,MAAM,KAAK,EAAE,QAAQ,GAAO,CAAC,CAAC,KAAK,GAAG,MACrC,kBAAC,OAAD;GAEE,WAAU;aAFZ;IAIE,kBAAC,GAAD;KAAc,OAAM;KAAQ,QAAO;KAAQ,CAAA;IAC3C,kBAAC,GAAD;KAAc,OAAM;KAAS,QAAO;KAAQ,CAAA;IAC5C,kBAAC,GAAD;KAAc,OAAM;KAAQ,QAAO;KAAQ,CAAA;IACvC;KANC,EAMD,CACN;EACE,CAAA;;AAUV,SAAgB,EAAa,EAAE,WAAQ,aAAU,QAAQ,WAAQ,KAAwB;AACvF,QACE,kBAAC,OAAD;EAAK,WAAU;EAAY,MAAK;EAAS,cAAW;YAApD,CACG,GACA,MAAY,UACX,kBAAC,GAAD,EAAsB,UAAS,CAAA,GAC7B,MAAY,WACd,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,GAAD;KAAc,OAAM;KAAQ,QAAO;KAAQ,CAAA;IAC3C,kBAAC,GAAD;KAAc,OAAM;KAAQ,QAAO;KAAQ,CAAA;IAC3C,kBAAC,GAAD;KAAc,OAAM;KAAS,QAAO;KAAQ,CAAA;IAC5C,kBAAC,GAAD;KAAc,OAAM;KAAQ,QAAO;KAAQ,CAAA;IAC3C,kBAAC,GAAD,EAAc,OAAO,KAAK,IAAI,IAAQ,GAAG,EAAE,EAAI,CAAA;IAC3C;OAEN,kBAAC,GAAD,EAAqB,UAAS,CAAA,CAE5B"}
1
+ {"version":3,"file":"PageSkeleton.js","names":[],"sources":["../../../src/components/shared/PageSkeleton.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\n\ninterface SkeletonLineProps {\n width?: string;\n height?: string;\n className?: string;\n}\n\nexport function SkeletonLine({\n width = 'w-full',\n height = 'h-4',\n className = '',\n}: SkeletonLineProps) {\n // Boff UI 2.0 — a real glass shimmer sweep instead of a flat opacity pulse.\n // The inner overlay sweeps left→right via `animate-vc-shimmer`; under reduced\n // motion the sweep is disabled (motion-safe) and the calm sunken fill remains.\n return (\n <div\n className={`relative overflow-hidden rounded bg-bg-sunken ${width} ${height} ${className}`}\n >\n <div className=\"absolute inset-0 -translate-x-full bg-[linear-gradient(90deg,transparent,color-mix(in_oklab,var(--color-text-primary)_8%,transparent),transparent)] motion-safe:animate-vc-shimmer\" />\n </div>\n );\n}\n\ninterface SkeletonRowsProps {\n count?: number;\n rowClassName?: string;\n}\n\nexport function SkeletonRows({ count = 6, rowClassName = '' }: SkeletonRowsProps) {\n return (\n <div className=\"space-y-3\" aria-hidden=\"true\">\n {Array.from({ length: count }).map((_, i) => (\n <div\n key={i}\n className={`flex items-center gap-3 rounded-card border border-border-subtle bg-bg-surface shadow-elevation-1 p-4 ${rowClassName}`}\n >\n <SkeletonLine width=\"w-8\" height=\"h-8\" className=\"rounded-full\" />\n <div className=\"flex-1 space-y-2\">\n <SkeletonLine width=\"w-1/3\" />\n <SkeletonLine width=\"w-1/2\" height=\"h-3\" />\n </div>\n <SkeletonLine width=\"w-16\" height=\"h-6\" />\n </div>\n ))}\n </div>\n );\n}\n\ninterface SkeletonCardsProps {\n count?: number;\n columns?: number;\n}\n\nexport function SkeletonCards({ count = 6, columns = 3 }: SkeletonCardsProps) {\n const gridCols =\n columns === 4\n ? 'sm:grid-cols-2 lg:grid-cols-4'\n : columns === 2\n ? 'sm:grid-cols-2'\n : 'sm:grid-cols-2 lg:grid-cols-3';\n return (\n <div className={`grid grid-cols-1 gap-4 ${gridCols}`} aria-hidden=\"true\">\n {Array.from({ length: count }).map((_, i) => (\n <div\n key={i}\n className=\"rounded-card border border-border-subtle bg-bg-surface shadow-elevation-1 p-5 space-y-3\"\n >\n <SkeletonLine width=\"w-2/3\" height=\"h-5\" />\n <SkeletonLine width=\"w-full\" height=\"h-3\" />\n <SkeletonLine width=\"w-1/2\" height=\"h-3\" />\n </div>\n ))}\n </div>\n );\n}\n\ninterface PageSkeletonProps {\n header?: ReactNode;\n variant?: 'rows' | 'cards' | 'detail';\n count?: number;\n}\n\nexport function PageSkeleton({ header, variant = 'rows', count = 6 }: PageSkeletonProps) {\n return (\n <div className=\"space-y-6\" role=\"status\" aria-label=\"Loading content\">\n {header}\n {variant === 'cards' ? (\n <SkeletonCards count={count} />\n ) : variant === 'detail' ? (\n <div className=\"space-y-3\">\n <SkeletonLine width=\"w-1/3\" height=\"h-6\" />\n <SkeletonLine width=\"w-2/3\" height=\"h-3\" />\n <SkeletonLine width=\"w-full\" height=\"h-3\" />\n <SkeletonLine width=\"w-3/4\" height=\"h-3\" />\n <SkeletonRows count={Math.max(count - 2, 3)} />\n </div>\n ) : (\n <SkeletonRows count={count} />\n )}\n </div>\n );\n}\n"],"mappings":";;AAQA,SAAgB,EAAa,EAC3B,WAAQ,UACR,YAAS,OACT,eAAY,MACQ;AAIpB,QACE,kBAAC,OAAD;EACE,WAAW,iDAAiD,EAAM,GAAG,EAAO,GAAG;YAE/E,kBAAC,OAAD,EAAK,WAAU,sLAAuL,CAAA;EAClM,CAAA;;AASV,SAAgB,EAAa,EAAE,WAAQ,GAAG,kBAAe,MAAyB;AAChF,QACE,kBAAC,OAAD;EAAK,WAAU;EAAY,eAAY;YACpC,MAAM,KAAK,EAAE,QAAQ,GAAO,CAAC,CAAC,KAAK,GAAG,MACrC,kBAAC,OAAD;GAEE,WAAW,yGAAyG;aAFtH;IAIE,kBAAC,GAAD;KAAc,OAAM;KAAM,QAAO;KAAM,WAAU;KAAiB,CAAA;IAClE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAc,OAAM,SAAU,CAAA,EAC9B,kBAAC,GAAD;MAAc,OAAM;MAAQ,QAAO;MAAQ,CAAA,CACvC;;IACN,kBAAC,GAAD;KAAc,OAAM;KAAO,QAAO;KAAQ,CAAA;IACtC;KATC,EASD,CACN;EACE,CAAA;;AASV,SAAgB,EAAc,EAAE,WAAQ,GAAG,aAAU,KAAyB;AAO5E,QACE,kBAAC,OAAD;EAAK,WAAW,0BANhB,MAAY,IACR,kCACA,MAAY,IACV,mBACA;EAEgD,eAAY;YAC/D,MAAM,KAAK,EAAE,QAAQ,GAAO,CAAC,CAAC,KAAK,GAAG,MACrC,kBAAC,OAAD;GAEE,WAAU;aAFZ;IAIE,kBAAC,GAAD;KAAc,OAAM;KAAQ,QAAO;KAAQ,CAAA;IAC3C,kBAAC,GAAD;KAAc,OAAM;KAAS,QAAO;KAAQ,CAAA;IAC5C,kBAAC,GAAD;KAAc,OAAM;KAAQ,QAAO;KAAQ,CAAA;IACvC;KANC,EAMD,CACN;EACE,CAAA;;AAUV,SAAgB,EAAa,EAAE,WAAQ,aAAU,QAAQ,WAAQ,KAAwB;AACvF,QACE,kBAAC,OAAD;EAAK,WAAU;EAAY,MAAK;EAAS,cAAW;YAApD,CACG,GACA,MAAY,UACX,kBAAC,GAAD,EAAsB,UAAS,CAAA,GAC7B,MAAY,WACd,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,GAAD;KAAc,OAAM;KAAQ,QAAO;KAAQ,CAAA;IAC3C,kBAAC,GAAD;KAAc,OAAM;KAAQ,QAAO;KAAQ,CAAA;IAC3C,kBAAC,GAAD;KAAc,OAAM;KAAS,QAAO;KAAQ,CAAA;IAC5C,kBAAC,GAAD;KAAc,OAAM;KAAQ,QAAO;KAAQ,CAAA;IAC3C,kBAAC,GAAD,EAAc,OAAO,KAAK,IAAI,IAAQ,GAAG,EAAE,EAAI,CAAA;IAC3C;OAEN,kBAAC,GAAD,EAAqB,UAAS,CAAA,CAE5B"}
@@ -9,11 +9,18 @@ function y({ status: t, onReconnect: n }) {
9
9
  return t === "disconnected" ? null : /* @__PURE__ */ v("div", {
10
10
  className: "flex items-center gap-2 px-3 py-1.5 rounded-lg bg-bg-secondary border border-border-primary text-sm",
11
11
  children: [
12
- t === "connected" && /* @__PURE__ */ v(g, { children: [/* @__PURE__ */ _(f, { className: "size-4 text-status-success-text" }), /* @__PURE__ */ _("span", {
13
- className: "text-status-success-text font-medium",
14
- children: r("vibecontrols.shared.sessionNotificationBanner.live", "Live")
15
- })] }),
16
- t === "connecting" && /* @__PURE__ */ v(g, { children: [/* @__PURE__ */ _(f, { className: "size-4 text-status-warning-text animate-pulse" }), /* @__PURE__ */ _("span", {
12
+ t === "connected" && /* @__PURE__ */ v(g, { children: [
13
+ /* @__PURE__ */ _("span", {
14
+ "aria-hidden": "true",
15
+ className: "size-2 rounded-full bg-status-success-text motion-safe:animate-vc-glow-pulse"
16
+ }),
17
+ /* @__PURE__ */ _(f, { className: "size-4 text-status-success-text" }),
18
+ /* @__PURE__ */ _("span", {
19
+ className: "text-status-success-text font-medium",
20
+ children: r("vibecontrols.shared.sessionNotificationBanner.live", "Live")
21
+ })
22
+ ] }),
23
+ t === "connecting" && /* @__PURE__ */ v(g, { children: [/* @__PURE__ */ _(f, { className: "size-4 text-status-warning-text motion-safe:animate-vc-glow-pulse" }), /* @__PURE__ */ _("span", {
17
24
  className: "text-status-warning-text",
18
25
  children: r("vibecontrols.shared.sessionNotificationBanner.connecting", "Connecting...")
19
26
  })] }),
@@ -1 +1 @@
1
- {"version":3,"file":"SessionNotificationBanner.js","names":[],"sources":["../../../src/components/shared/SessionNotificationBanner.tsx"],"sourcesContent":["/**\n * SessionNotificationBanner\n *\n * Displays AI tool notifications contextually within the Sessions page,\n * positioned above the session list for immediate visibility.\n *\n * Features:\n * - Source-specific styling (OpenCode, Claude Code, Gemini, Cursor)\n * - Event-type icons (success, error, info)\n * - Separate notification state from global toasts (no auto-dismiss interference)\n * - Manual dismiss button\n * - Smooth enter/exit animations\n *\n * Architecture:\n * - SessionNotifications maintains its OWN notification state separate from the global toast system\n * - It subscribes to the same WebSocket events but stores notifications independently\n * - This allows global toasts to auto-dismiss without affecting session banners\n */\nimport { useEffect, useState, useCallback, useRef } from 'react';\nimport {\n Bell,\n CheckCircle,\n AlertCircle,\n Info,\n X,\n Sparkles,\n Terminal,\n Zap,\n Wifi,\n WifiOff,\n} from 'lucide-react';\n\nimport type { AiToolNotification, ConnectionStatus } from '../../hooks/useAgentEvents';\nimport { useAgentEventsContext } from '../../providers/AgentEventsProvider';\nimport { useTr } from '../../shared/hooks/useTr';\n\n/**\n * Props for the connection status indicator\n */\ninterface ConnectionStatusIndicatorProps {\n status: ConnectionStatus;\n onReconnect: () => void;\n}\n\n/**\n * Connection status indicator showing WebSocket state\n */\nexport function ConnectionStatusIndicator({ status, onReconnect }: ConnectionStatusIndicatorProps) {\n const tr = useTr();\n\n if (status === 'disconnected') {\n return null;\n }\n\n return (\n <div className=\"flex items-center gap-2 px-3 py-1.5 rounded-lg bg-bg-secondary border border-border-primary text-sm\">\n {status === 'connected' && (\n <>\n <Wifi className=\"size-4 text-status-success-text\" />\n <span className=\"text-status-success-text font-medium\">\n {tr('vibecontrols.shared.sessionNotificationBanner.live', 'Live')}\n </span>\n </>\n )}\n {status === 'connecting' && (\n <>\n <Wifi className=\"size-4 text-status-warning-text animate-pulse\" />\n <span className=\"text-status-warning-text\">\n {tr('vibecontrols.shared.sessionNotificationBanner.connecting', 'Connecting...')}\n </span>\n </>\n )}\n {status === 'error' && (\n <>\n <WifiOff className=\"size-4 text-status-error-text\" />\n <span className=\"text-status-error-text\">\n {tr('vibecontrols.shared.sessionNotificationBanner.disconnected', 'Disconnected')}\n </span>\n <button\n type=\"button\"\n onClick={onReconnect}\n className=\"ml-2 px-2 py-0.5 text-xs rounded bg-accent-primary/10 text-accent-primary hover:bg-accent-primary/20 transition-colors\"\n >\n {tr('vibecontrols.shared.sessionNotificationBanner.reconnect', 'Reconnect')}\n </button>\n </>\n )}\n </div>\n );\n}\n\n/**\n * Props for a single notification banner\n */\ninterface NotificationBannerProps {\n notification: AiToolNotification;\n onDismiss: (id: string) => void;\n autoDismiss?: boolean;\n autoDismissDelay?: number;\n}\n\n/**\n * Get the icon for a source tool\n */\nfunction getSourceIcon(source: string) {\n const iconClass = 'size-5';\n\n switch (source.toLowerCase()) {\n case 'opencode':\n return <Terminal className={iconClass} />;\n case 'claude-code':\n case 'claudecode':\n return <Sparkles className={iconClass} />;\n case 'gemini-cli':\n case 'gemini':\n return <Zap className={iconClass} />;\n case 'cursor':\n return <Terminal className={iconClass} />;\n default:\n return <Bell className={iconClass} />;\n }\n}\n\n/**\n * Get the icon for an event type\n */\nfunction getEventIcon(eventType: string) {\n const iconClass = 'size-4';\n\n if (\n eventType.includes('completed') ||\n eventType.includes('success') ||\n eventType.includes('idle')\n ) {\n return <CheckCircle className={`${iconClass} text-status-success-text`} />;\n }\n if (eventType.includes('failed') || eventType.includes('error')) {\n return <AlertCircle className={`${iconClass} text-status-error-text`} />;\n }\n if (eventType.includes('question') || eventType.includes('permission')) {\n return <AlertCircle className={`${iconClass} text-status-warning-text`} />;\n }\n return <Info className={`${iconClass} text-status-info-text`} />;\n}\n\n/**\n * Get display name for a source tool\n */\nfunction getSourceDisplayName(source: string): string {\n const names: Record<string, string> = {\n opencode: 'OpenCode',\n 'opencode-vibe-webhook': 'OpenCode',\n 'claude-code': 'Claude Code',\n claudecode: 'Claude Code',\n 'gemini-cli': 'Gemini CLI',\n gemini: 'Gemini CLI',\n cursor: 'Cursor',\n };\n return names[source.toLowerCase()] || source;\n}\n\n/**\n * Get background color based on source\n */\nfunction getSourceStyles(source: string): { bg: string; border: string; text: string } {\n switch (source.toLowerCase()) {\n case 'opencode':\n case 'opencode-vibe-webhook':\n return {\n bg: 'bg-status-info-bg/10',\n border: 'border-status-info-border',\n text: 'text-status-info-text',\n };\n case 'claude-code':\n case 'claudecode':\n return {\n bg: 'bg-status-warning-bg/10',\n border: 'border-status-warning-border',\n text: 'text-status-warning-text',\n };\n case 'gemini-cli':\n case 'gemini':\n return {\n bg: 'bg-status-info-bg/10',\n border: 'border-status-info-border',\n text: 'text-status-info-text',\n };\n case 'cursor':\n return {\n bg: 'bg-status-success-bg/10',\n border: 'border-status-success-border',\n text: 'text-status-success-text',\n };\n default:\n return {\n bg: 'bg-bg-secondary',\n border: 'border-border-primary',\n text: 'text-text-secondary',\n };\n }\n}\n\n/**\n * Format timestamp for display\n */\nfunction formatTimestamp(timestamp: string): string {\n try {\n const date = new Date(timestamp);\n return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });\n } catch {\n return '';\n }\n}\n\n/**\n * Single notification banner\n */\nfunction NotificationBanner({\n notification,\n onDismiss,\n autoDismiss = true,\n autoDismissDelay = 8000,\n}: NotificationBannerProps) {\n const [isExiting, setIsExiting] = useState(false);\n const [progress, setProgress] = useState(100);\n const styles = getSourceStyles(notification.source);\n\n useEffect(() => {\n if (!autoDismiss) return;\n\n // Progress countdown\n const startTime = Date.now();\n const interval = setInterval(() => {\n const elapsed = Date.now() - startTime;\n const remaining = Math.max(0, 100 - (elapsed / autoDismissDelay) * 100);\n setProgress(remaining);\n }, 50);\n\n // Auto-dismiss timer\n const timer = setTimeout(() => {\n setIsExiting(true);\n setTimeout(() => onDismiss(notification.id), 300);\n }, autoDismissDelay);\n\n return () => {\n clearInterval(interval);\n clearTimeout(timer);\n };\n }, [autoDismiss, autoDismissDelay, notification.id, onDismiss]);\n\n const handleDismiss = () => {\n setIsExiting(true);\n setTimeout(() => onDismiss(notification.id), 300);\n };\n\n return (\n <div\n className={`\n relative flex items-start gap-3 p-4 rounded-lg border shadow-lg backdrop-blur-sm\n ${styles.bg} ${styles.border}\n transform transition-all duration-300 ease-out\n ${isExiting ? 'opacity-0 -translate-y-2 scale-95' : 'opacity-100 translate-y-0 scale-100'}\n animate-slide-in-down\n `}\n role=\"alert\"\n data-testid=\"session-notification-banner\"\n >\n {/* Source icon */}\n <div className={`flex-shrink-0 p-2 rounded-lg bg-bg-tertiary/50 ${styles.text}`}>\n {getSourceIcon(notification.source)}\n </div>\n\n {/* Content */}\n <div className=\"flex-1 min-w-0\">\n {/* Header */}\n <div className=\"flex items-center gap-2 mb-1\">\n <span className={`text-xs font-semibold uppercase tracking-wide ${styles.text}`}>\n {getSourceDisplayName(notification.source)}\n </span>\n {getEventIcon(notification.eventType)}\n <span className=\"text-xs text-text-muted\">{formatTimestamp(notification.timestamp)}</span>\n </div>\n\n {/* Title */}\n <p className=\"text-sm font-medium text-text-primary\">{notification.title}</p>\n\n {/* Message */}\n <p className=\"text-xs text-text-secondary mt-0.5\">{notification.message}</p>\n </div>\n\n {/* Dismiss button */}\n <button\n type=\"button\"\n onClick={handleDismiss}\n className=\"flex-shrink-0 p-1 rounded-md hover:bg-bg-tertiary/50 text-text-muted hover:text-text-secondary transition-colors\"\n aria-label=\"Dismiss notification\"\n >\n <X className=\"size-4\" />\n </button>\n\n {/* Progress bar for auto-dismiss */}\n {autoDismiss && (\n <div className=\"absolute bottom-0 left-0 right-0 h-0.5 bg-bg-tertiary/30 overflow-hidden rounded-b-lg\">\n <div\n className={`h-full ${styles.text.replace('text-', 'bg-')}/50 transition-all duration-50 ease-linear`}\n style={{ width: `${progress}%` }}\n />\n </div>\n )}\n </div>\n );\n}\n\n/**\n * Props for the session notifications container\n */\nexport interface SessionNotificationsProps {\n /**\n * Maximum number of notifications to display\n */\n maxVisible?: number;\n\n /**\n * Whether notifications should auto-dismiss (default: false - requires manual close)\n */\n autoDismiss?: boolean;\n\n /**\n * Auto-dismiss delay in milliseconds (only used if autoDismiss is true)\n */\n autoDismissDelay?: number;\n\n /**\n * Whether to show the connection status indicator\n */\n showConnectionStatus?: boolean;\n\n /**\n * Compact mode: drop the standalone \"Agent Notifications\" header row and\n * render nothing at all when there are no notifications, so the banner\n * stops reserving vertical space. The connection status is expected to be\n * rendered in the page header instead (see `SessionConnectionStatus`).\n */\n compact?: boolean;\n}\n\n/**\n * Standalone agent connection-status chip for placement in a page header.\n * Reclaims the row the notification banner used to spend on a dedicated\n * \"Agent Notifications\" + status line. Renders nothing when no agent is\n * configured for the current context.\n */\nexport function SessionConnectionStatus() {\n const { connectionStatus, reconnect, agentId } = useAgentEventsContext();\n if (!agentId) return null;\n return <ConnectionStatusIndicator status={connectionStatus} onReconnect={reconnect} />;\n}\n\n/**\n * Maximum notifications to keep in session banner state\n */\nconst MAX_SESSION_NOTIFICATIONS = 50;\n\n/**\n * Container component for session-contextual notifications\n * Displays notifications above the session list with connection status\n *\n * IMPORTANT: This component maintains its OWN notification state separate from the global\n * toast system. It subscribes to the same WebSocket events via the context but stores\n * notifications independently. This allows:\n * - Global toasts to auto-dismiss without affecting session banners\n * - Session banners to persist until manually dismissed by the user\n */\nexport function SessionNotifications({\n maxVisible = 3,\n autoDismiss = false,\n autoDismissDelay = 8000,\n showConnectionStatus = true,\n compact = false,\n}: SessionNotificationsProps) {\n const tr = useTr();\n const {\n notifications: globalNotifications,\n connectionStatus,\n reconnect,\n agentId,\n } = useAgentEventsContext();\n\n // Maintain separate notification state for session banners\n const [sessionNotifications, setSessionNotifications] = useState<AiToolNotification[]>([]);\n const seenIdsRef = useRef<Set<string>>(new Set());\n\n // Sync new notifications from global state to session state\n // We only ADD new notifications, never remove them (except via manual dismiss)\n useEffect(() => {\n const newNotifications: AiToolNotification[] = [];\n\n for (const notification of globalNotifications) {\n if (!seenIdsRef.current.has(notification.id)) {\n seenIdsRef.current.add(notification.id);\n newNotifications.push(notification);\n }\n }\n\n if (newNotifications.length > 0) {\n setSessionNotifications((prev) => {\n const updated = [...newNotifications, ...prev];\n return updated.slice(0, MAX_SESSION_NOTIFICATIONS);\n });\n }\n }, [globalNotifications]);\n\n // Handle manual dismiss - only affects session banner state\n const handleDismiss = useCallback((id: string) => {\n setSessionNotifications((prev) => prev.filter((n) => n.id !== id));\n }, []);\n\n const visibleNotifications = sessionNotifications.slice(0, maxVisible);\n const hiddenCount = Math.max(0, sessionNotifications.length - maxVisible);\n\n // Don't render anything if no agent is configured. In compact mode we also\n // render nothing while there are no notifications so the banner stops\n // reserving a wasted row (the connection status lives in the page header).\n if (!agentId && sessionNotifications.length === 0) {\n return null;\n }\n if (compact && sessionNotifications.length === 0) {\n return null;\n }\n\n return (\n <div\n className={compact ? 'space-y-2' : 'mb-4 space-y-3'}\n data-testid=\"session-notifications-container\"\n >\n {/* Connection status indicator — header row only in full mode. In\n compact mode the status is rendered in the page header instead. */}\n {!compact && showConnectionStatus && agentId && (\n <div className=\"flex items-center justify-between\">\n <div className=\"text-xs text-text-muted\">\n {tr(\n 'vibecontrols.shared.sessionNotificationBanner.agentNotifications',\n 'Agent Notifications'\n )}\n </div>\n <ConnectionStatusIndicator status={connectionStatus} onReconnect={reconnect} />\n </div>\n )}\n\n {/* Notification banners */}\n <div className=\"space-y-2\">\n {visibleNotifications.map((notification) => (\n <NotificationBanner\n key={notification.id}\n notification={notification}\n onDismiss={handleDismiss}\n autoDismiss={autoDismiss}\n autoDismissDelay={autoDismissDelay}\n />\n ))}\n </div>\n\n {/* Hidden count indicator */}\n {hiddenCount > 0 && (\n <div className=\"text-xs text-text-muted text-center py-1\">\n +{hiddenCount}{' '}\n {tr(\n 'vibecontrols.shared.sessionNotificationBanner.moreNotifications',\n 'more notification(s)'\n )}\n </div>\n )}\n </div>\n );\n}\n\nexport default SessionNotifications;\n"],"mappings":"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"}
1
+ {"version":3,"file":"SessionNotificationBanner.js","names":[],"sources":["../../../src/components/shared/SessionNotificationBanner.tsx"],"sourcesContent":["/**\n * SessionNotificationBanner\n *\n * Displays AI tool notifications contextually within the Sessions page,\n * positioned above the session list for immediate visibility.\n *\n * Features:\n * - Source-specific styling (OpenCode, Claude Code, Gemini, Cursor)\n * - Event-type icons (success, error, info)\n * - Separate notification state from global toasts (no auto-dismiss interference)\n * - Manual dismiss button\n * - Smooth enter/exit animations\n *\n * Architecture:\n * - SessionNotifications maintains its OWN notification state separate from the global toast system\n * - It subscribes to the same WebSocket events but stores notifications independently\n * - This allows global toasts to auto-dismiss without affecting session banners\n */\nimport { useEffect, useState, useCallback, useRef } from 'react';\nimport {\n Bell,\n CheckCircle,\n AlertCircle,\n Info,\n X,\n Sparkles,\n Terminal,\n Zap,\n Wifi,\n WifiOff,\n} from 'lucide-react';\n\nimport type { AiToolNotification, ConnectionStatus } from '../../hooks/useAgentEvents';\nimport { useAgentEventsContext } from '../../providers/AgentEventsProvider';\nimport { useTr } from '../../shared/hooks/useTr';\n\n/**\n * Props for the connection status indicator\n */\ninterface ConnectionStatusIndicatorProps {\n status: ConnectionStatus;\n onReconnect: () => void;\n}\n\n/**\n * Connection status indicator showing WebSocket state\n */\nexport function ConnectionStatusIndicator({ status, onReconnect }: ConnectionStatusIndicatorProps) {\n const tr = useTr();\n\n if (status === 'disconnected') {\n return null;\n }\n\n return (\n <div className=\"flex items-center gap-2 px-3 py-1.5 rounded-lg bg-bg-secondary border border-border-primary text-sm\">\n {status === 'connected' && (\n <>\n <span\n aria-hidden=\"true\"\n className=\"size-2 rounded-full bg-status-success-text motion-safe:animate-vc-glow-pulse\"\n />\n <Wifi className=\"size-4 text-status-success-text\" />\n <span className=\"text-status-success-text font-medium\">\n {tr('vibecontrols.shared.sessionNotificationBanner.live', 'Live')}\n </span>\n </>\n )}\n {status === 'connecting' && (\n <>\n <Wifi className=\"size-4 text-status-warning-text motion-safe:animate-vc-glow-pulse\" />\n <span className=\"text-status-warning-text\">\n {tr('vibecontrols.shared.sessionNotificationBanner.connecting', 'Connecting...')}\n </span>\n </>\n )}\n {status === 'error' && (\n <>\n <WifiOff className=\"size-4 text-status-error-text\" />\n <span className=\"text-status-error-text\">\n {tr('vibecontrols.shared.sessionNotificationBanner.disconnected', 'Disconnected')}\n </span>\n <button\n type=\"button\"\n onClick={onReconnect}\n className=\"ml-2 px-2 py-0.5 text-xs rounded bg-accent-primary/10 text-accent-primary hover:bg-accent-primary/20 transition-colors\"\n >\n {tr('vibecontrols.shared.sessionNotificationBanner.reconnect', 'Reconnect')}\n </button>\n </>\n )}\n </div>\n );\n}\n\n/**\n * Props for a single notification banner\n */\ninterface NotificationBannerProps {\n notification: AiToolNotification;\n onDismiss: (id: string) => void;\n autoDismiss?: boolean;\n autoDismissDelay?: number;\n}\n\n/**\n * Get the icon for a source tool\n */\nfunction getSourceIcon(source: string) {\n const iconClass = 'size-5';\n\n switch (source.toLowerCase()) {\n case 'opencode':\n return <Terminal className={iconClass} />;\n case 'claude-code':\n case 'claudecode':\n return <Sparkles className={iconClass} />;\n case 'gemini-cli':\n case 'gemini':\n return <Zap className={iconClass} />;\n case 'cursor':\n return <Terminal className={iconClass} />;\n default:\n return <Bell className={iconClass} />;\n }\n}\n\n/**\n * Get the icon for an event type\n */\nfunction getEventIcon(eventType: string) {\n const iconClass = 'size-4';\n\n if (\n eventType.includes('completed') ||\n eventType.includes('success') ||\n eventType.includes('idle')\n ) {\n return <CheckCircle className={`${iconClass} text-status-success-text`} />;\n }\n if (eventType.includes('failed') || eventType.includes('error')) {\n return <AlertCircle className={`${iconClass} text-status-error-text`} />;\n }\n if (eventType.includes('question') || eventType.includes('permission')) {\n return <AlertCircle className={`${iconClass} text-status-warning-text`} />;\n }\n return <Info className={`${iconClass} text-status-info-text`} />;\n}\n\n/**\n * Get display name for a source tool\n */\nfunction getSourceDisplayName(source: string): string {\n const names: Record<string, string> = {\n opencode: 'OpenCode',\n 'opencode-vibe-webhook': 'OpenCode',\n 'claude-code': 'Claude Code',\n claudecode: 'Claude Code',\n 'gemini-cli': 'Gemini CLI',\n gemini: 'Gemini CLI',\n cursor: 'Cursor',\n };\n return names[source.toLowerCase()] || source;\n}\n\n/**\n * Get background color based on source\n */\nfunction getSourceStyles(source: string): { bg: string; border: string; text: string } {\n switch (source.toLowerCase()) {\n case 'opencode':\n case 'opencode-vibe-webhook':\n return {\n bg: 'bg-status-info-bg/10',\n border: 'border-status-info-border',\n text: 'text-status-info-text',\n };\n case 'claude-code':\n case 'claudecode':\n return {\n bg: 'bg-status-warning-bg/10',\n border: 'border-status-warning-border',\n text: 'text-status-warning-text',\n };\n case 'gemini-cli':\n case 'gemini':\n return {\n bg: 'bg-status-info-bg/10',\n border: 'border-status-info-border',\n text: 'text-status-info-text',\n };\n case 'cursor':\n return {\n bg: 'bg-status-success-bg/10',\n border: 'border-status-success-border',\n text: 'text-status-success-text',\n };\n default:\n return {\n bg: 'bg-bg-secondary',\n border: 'border-border-primary',\n text: 'text-text-secondary',\n };\n }\n}\n\n/**\n * Format timestamp for display\n */\nfunction formatTimestamp(timestamp: string): string {\n try {\n const date = new Date(timestamp);\n return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });\n } catch {\n return '';\n }\n}\n\n/**\n * Single notification banner\n */\nfunction NotificationBanner({\n notification,\n onDismiss,\n autoDismiss = true,\n autoDismissDelay = 8000,\n}: NotificationBannerProps) {\n const [isExiting, setIsExiting] = useState(false);\n const [progress, setProgress] = useState(100);\n const styles = getSourceStyles(notification.source);\n\n useEffect(() => {\n if (!autoDismiss) return;\n\n // Progress countdown\n const startTime = Date.now();\n const interval = setInterval(() => {\n const elapsed = Date.now() - startTime;\n const remaining = Math.max(0, 100 - (elapsed / autoDismissDelay) * 100);\n setProgress(remaining);\n }, 50);\n\n // Auto-dismiss timer\n const timer = setTimeout(() => {\n setIsExiting(true);\n setTimeout(() => onDismiss(notification.id), 300);\n }, autoDismissDelay);\n\n return () => {\n clearInterval(interval);\n clearTimeout(timer);\n };\n }, [autoDismiss, autoDismissDelay, notification.id, onDismiss]);\n\n const handleDismiss = () => {\n setIsExiting(true);\n setTimeout(() => onDismiss(notification.id), 300);\n };\n\n return (\n <div\n className={`\n relative flex items-start gap-3 p-4 rounded-lg border shadow-lg backdrop-blur-sm\n ${styles.bg} ${styles.border}\n transform transition-all duration-300 ease-out\n ${isExiting ? 'opacity-0 -translate-y-2 scale-95' : 'opacity-100 translate-y-0 scale-100'}\n animate-slide-in-down\n `}\n role=\"alert\"\n data-testid=\"session-notification-banner\"\n >\n {/* Source icon */}\n <div className={`flex-shrink-0 p-2 rounded-lg bg-bg-tertiary/50 ${styles.text}`}>\n {getSourceIcon(notification.source)}\n </div>\n\n {/* Content */}\n <div className=\"flex-1 min-w-0\">\n {/* Header */}\n <div className=\"flex items-center gap-2 mb-1\">\n <span className={`text-xs font-semibold uppercase tracking-wide ${styles.text}`}>\n {getSourceDisplayName(notification.source)}\n </span>\n {getEventIcon(notification.eventType)}\n <span className=\"text-xs text-text-muted\">{formatTimestamp(notification.timestamp)}</span>\n </div>\n\n {/* Title */}\n <p className=\"text-sm font-medium text-text-primary\">{notification.title}</p>\n\n {/* Message */}\n <p className=\"text-xs text-text-secondary mt-0.5\">{notification.message}</p>\n </div>\n\n {/* Dismiss button */}\n <button\n type=\"button\"\n onClick={handleDismiss}\n className=\"flex-shrink-0 p-1 rounded-md hover:bg-bg-tertiary/50 text-text-muted hover:text-text-secondary transition-colors\"\n aria-label=\"Dismiss notification\"\n >\n <X className=\"size-4\" />\n </button>\n\n {/* Progress bar for auto-dismiss */}\n {autoDismiss && (\n <div className=\"absolute bottom-0 left-0 right-0 h-0.5 bg-bg-tertiary/30 overflow-hidden rounded-b-lg\">\n <div\n className={`h-full ${styles.text.replace('text-', 'bg-')}/50 transition-all duration-50 ease-linear`}\n style={{ width: `${progress}%` }}\n />\n </div>\n )}\n </div>\n );\n}\n\n/**\n * Props for the session notifications container\n */\nexport interface SessionNotificationsProps {\n /**\n * Maximum number of notifications to display\n */\n maxVisible?: number;\n\n /**\n * Whether notifications should auto-dismiss (default: false - requires manual close)\n */\n autoDismiss?: boolean;\n\n /**\n * Auto-dismiss delay in milliseconds (only used if autoDismiss is true)\n */\n autoDismissDelay?: number;\n\n /**\n * Whether to show the connection status indicator\n */\n showConnectionStatus?: boolean;\n\n /**\n * Compact mode: drop the standalone \"Agent Notifications\" header row and\n * render nothing at all when there are no notifications, so the banner\n * stops reserving vertical space. The connection status is expected to be\n * rendered in the page header instead (see `SessionConnectionStatus`).\n */\n compact?: boolean;\n}\n\n/**\n * Standalone agent connection-status chip for placement in a page header.\n * Reclaims the row the notification banner used to spend on a dedicated\n * \"Agent Notifications\" + status line. Renders nothing when no agent is\n * configured for the current context.\n */\nexport function SessionConnectionStatus() {\n const { connectionStatus, reconnect, agentId } = useAgentEventsContext();\n if (!agentId) return null;\n return <ConnectionStatusIndicator status={connectionStatus} onReconnect={reconnect} />;\n}\n\n/**\n * Maximum notifications to keep in session banner state\n */\nconst MAX_SESSION_NOTIFICATIONS = 50;\n\n/**\n * Container component for session-contextual notifications\n * Displays notifications above the session list with connection status\n *\n * IMPORTANT: This component maintains its OWN notification state separate from the global\n * toast system. It subscribes to the same WebSocket events via the context but stores\n * notifications independently. This allows:\n * - Global toasts to auto-dismiss without affecting session banners\n * - Session banners to persist until manually dismissed by the user\n */\nexport function SessionNotifications({\n maxVisible = 3,\n autoDismiss = false,\n autoDismissDelay = 8000,\n showConnectionStatus = true,\n compact = false,\n}: SessionNotificationsProps) {\n const tr = useTr();\n const {\n notifications: globalNotifications,\n connectionStatus,\n reconnect,\n agentId,\n } = useAgentEventsContext();\n\n // Maintain separate notification state for session banners\n const [sessionNotifications, setSessionNotifications] = useState<AiToolNotification[]>([]);\n const seenIdsRef = useRef<Set<string>>(new Set());\n\n // Sync new notifications from global state to session state\n // We only ADD new notifications, never remove them (except via manual dismiss)\n useEffect(() => {\n const newNotifications: AiToolNotification[] = [];\n\n for (const notification of globalNotifications) {\n if (!seenIdsRef.current.has(notification.id)) {\n seenIdsRef.current.add(notification.id);\n newNotifications.push(notification);\n }\n }\n\n if (newNotifications.length > 0) {\n setSessionNotifications((prev) => {\n const updated = [...newNotifications, ...prev];\n return updated.slice(0, MAX_SESSION_NOTIFICATIONS);\n });\n }\n }, [globalNotifications]);\n\n // Handle manual dismiss - only affects session banner state\n const handleDismiss = useCallback((id: string) => {\n setSessionNotifications((prev) => prev.filter((n) => n.id !== id));\n }, []);\n\n const visibleNotifications = sessionNotifications.slice(0, maxVisible);\n const hiddenCount = Math.max(0, sessionNotifications.length - maxVisible);\n\n // Don't render anything if no agent is configured. In compact mode we also\n // render nothing while there are no notifications so the banner stops\n // reserving a wasted row (the connection status lives in the page header).\n if (!agentId && sessionNotifications.length === 0) {\n return null;\n }\n if (compact && sessionNotifications.length === 0) {\n return null;\n }\n\n return (\n <div\n className={compact ? 'space-y-2' : 'mb-4 space-y-3'}\n data-testid=\"session-notifications-container\"\n >\n {/* Connection status indicator — header row only in full mode. In\n compact mode the status is rendered in the page header instead. */}\n {!compact && showConnectionStatus && agentId && (\n <div className=\"flex items-center justify-between\">\n <div className=\"text-xs text-text-muted\">\n {tr(\n 'vibecontrols.shared.sessionNotificationBanner.agentNotifications',\n 'Agent Notifications'\n )}\n </div>\n <ConnectionStatusIndicator status={connectionStatus} onReconnect={reconnect} />\n </div>\n )}\n\n {/* Notification banners */}\n <div className=\"space-y-2\">\n {visibleNotifications.map((notification) => (\n <NotificationBanner\n key={notification.id}\n notification={notification}\n onDismiss={handleDismiss}\n autoDismiss={autoDismiss}\n autoDismissDelay={autoDismissDelay}\n />\n ))}\n </div>\n\n {/* Hidden count indicator */}\n {hiddenCount > 0 && (\n <div className=\"text-xs text-text-muted text-center py-1\">\n +{hiddenCount}{' '}\n {tr(\n 'vibecontrols.shared.sessionNotificationBanner.moreNotifications',\n 'more notification(s)'\n )}\n </div>\n )}\n </div>\n );\n}\n\nexport default SessionNotifications;\n"],"mappings":"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"}
@@ -51,10 +51,10 @@ function o({ status: o, variant: s, size: c = "sm", showDot: l = !0, pulse: u =
51
51
  let d = s || r[o] || "neutral", f = o.replace(/_/g, " ").toLowerCase(), p = {
52
52
  sm: "text-xs px-2 py-0.5",
53
53
  md: "text-sm px-2.5 py-1"
54
- }, m = c === "sm" ? "size-2" : "size-2.5";
54
+ }, m = c === "sm" ? "size-2" : "size-2.5", h = /^(ACTIVE|RUNNING|connected|connecting|STARTING|healthy)$/i.test(o), g = u || h;
55
55
  return /* @__PURE__ */ n("span", {
56
56
  className: `inline-flex items-center gap-1.5 font-medium rounded-full capitalize ${p[c]} ${i[d]}`,
57
- children: [l && /* @__PURE__ */ t(e, { className: `${m} fill-current ${a[d]} ${u ? "animate-pulse" : ""}` }), f]
57
+ children: [l && /* @__PURE__ */ t(e, { className: `${m} fill-current ${a[d]} ${g ? "motion-safe:animate-vc-glow-pulse" : ""}` }), f]
58
58
  });
59
59
  }
60
60
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"StatusBadge.js","names":[],"sources":["../../../src/components/shared/StatusBadge.tsx"],"sourcesContent":["import { Circle } from 'lucide-react';\n\ntype StatusVariant = 'success' | 'warning' | 'error' | 'info' | 'neutral';\n\ninterface StatusBadgeProps {\n status: string;\n variant?: StatusVariant;\n size?: 'sm' | 'md';\n showDot?: boolean;\n pulse?: boolean;\n}\n\nconst variantMap: Record<string, StatusVariant> = {\n // Agent statuses\n ACTIVE: 'success',\n RUNNING: 'success',\n IDLE: 'info',\n REGISTERING: 'info',\n DEGRADED: 'warning',\n OFFLINE: 'error',\n DELETED: 'neutral',\n // Session statuses\n PENDING: 'info',\n STARTING: 'info',\n STOPPING: 'warning',\n STOPPED: 'neutral',\n ERROR: 'error',\n TERMINATED: 'neutral',\n // Vibe statuses\n INITIALIZING: 'info',\n INACTIVE: 'neutral',\n ARCHIVED: 'neutral',\n // Port forward statuses\n CONFLICT: 'error',\n // Module statuses\n DRAFT: 'neutral',\n PENDING_REVIEW: 'info',\n APPROVED: 'success',\n REJECTED: 'error',\n DEPRECATED: 'warning',\n // Installation statuses\n INSTALLING: 'info',\n UNINSTALLING: 'warning',\n // Connection statuses\n connected: 'success',\n disconnected: 'neutral',\n connecting: 'info',\n error: 'error',\n // Health statuses\n healthy: 'success',\n degraded: 'warning',\n unhealthy: 'error',\n unknown: 'neutral',\n};\n\n// `info` (connecting / installing transient states) reads NEUTRAL rather than\n// off-brand blue — the product brand is purple and loud blue status pills\n// clash with it. Semantic hues stay for success/warning/error; purple is\n// reserved for primary actions, not badges. All use the subtle recipe so they\n// pass AA in light and dark.\nconst variantStyles: Record<StatusVariant, string> = {\n success: 'bg-status-success-bg-subtle text-status-success-text',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text',\n error: 'bg-status-error-bg-subtle text-status-error-text',\n info: 'bg-bg-sunken text-text-secondary',\n neutral: 'bg-bg-sunken text-text-secondary',\n};\n\nconst dotStyles: Record<StatusVariant, string> = {\n success: 'text-status-success-text',\n warning: 'text-status-warning-text',\n error: 'text-status-error-text',\n info: 'text-text-secondary',\n neutral: 'text-text-secondary',\n};\n\nexport function StatusBadge({\n status,\n variant,\n size = 'sm',\n showDot = true,\n pulse = false,\n}: StatusBadgeProps) {\n const resolvedVariant = variant || variantMap[status] || 'neutral';\n const displayStatus = status.replace(/_/g, ' ').toLowerCase();\n\n const sizeStyles = {\n sm: 'text-xs px-2 py-0.5',\n md: 'text-sm px-2.5 py-1',\n };\n\n const dotSize = size === 'sm' ? 'size-2' : 'size-2.5';\n\n return (\n <span\n className={`inline-flex items-center gap-1.5 font-medium rounded-full capitalize ${sizeStyles[size]} ${variantStyles[resolvedVariant]}`}\n >\n {showDot && (\n <Circle\n className={`${dotSize} fill-current ${dotStyles[resolvedVariant]} ${pulse ? 'animate-pulse' : ''}`}\n />\n )}\n {displayStatus}\n </span>\n );\n}\n"],"mappings":";;;AAYA,IAAM,IAA4C;CAEhD,QAAQ;CACR,SAAS;CACT,MAAM;CACN,aAAa;CACb,UAAU;CACV,SAAS;CACT,SAAS;CAET,SAAS;CACT,UAAU;CACV,UAAU;CACV,SAAS;CACT,OAAO;CACP,YAAY;CAEZ,cAAc;CACd,UAAU;CACV,UAAU;CAEV,UAAU;CAEV,OAAO;CACP,gBAAgB;CAChB,UAAU;CACV,UAAU;CACV,YAAY;CAEZ,YAAY;CACZ,cAAc;CAEd,WAAW;CACX,cAAc;CACd,YAAY;CACZ,OAAO;CAEP,SAAS;CACT,UAAU;CACV,WAAW;CACX,SAAS;CACV,EAOK,IAA+C;CACnD,SAAS;CACT,SAAS;CACT,OAAO;CACP,MAAM;CACN,SAAS;CACV,EAEK,IAA2C;CAC/C,SAAS;CACT,SAAS;CACT,OAAO;CACP,MAAM;CACN,SAAS;CACV;AAED,SAAgB,EAAY,EAC1B,WACA,YACA,UAAO,MACP,aAAU,IACV,WAAQ,MACW;CACnB,IAAM,IAAkB,KAAW,EAAW,MAAW,WACnD,IAAgB,EAAO,QAAQ,MAAM,IAAI,CAAC,aAAa,EAEvD,IAAa;EACjB,IAAI;EACJ,IAAI;EACL,EAEK,IAAU,MAAS,OAAO,WAAW;AAE3C,QACE,kBAAC,QAAD;EACE,WAAW,wEAAwE,EAAW,GAAM,GAAG,EAAc;YADvH,CAGG,KACC,kBAAC,GAAD,EACE,WAAW,GAAG,EAAQ,gBAAgB,EAAU,GAAiB,GAAG,IAAQ,kBAAkB,MAC9F,CAAA,EAEH,EACI"}
1
+ {"version":3,"file":"StatusBadge.js","names":[],"sources":["../../../src/components/shared/StatusBadge.tsx"],"sourcesContent":["import { Circle } from 'lucide-react';\n\ntype StatusVariant = 'success' | 'warning' | 'error' | 'info' | 'neutral';\n\ninterface StatusBadgeProps {\n status: string;\n variant?: StatusVariant;\n size?: 'sm' | 'md';\n showDot?: boolean;\n pulse?: boolean;\n}\n\nconst variantMap: Record<string, StatusVariant> = {\n // Agent statuses\n ACTIVE: 'success',\n RUNNING: 'success',\n IDLE: 'info',\n REGISTERING: 'info',\n DEGRADED: 'warning',\n OFFLINE: 'error',\n DELETED: 'neutral',\n // Session statuses\n PENDING: 'info',\n STARTING: 'info',\n STOPPING: 'warning',\n STOPPED: 'neutral',\n ERROR: 'error',\n TERMINATED: 'neutral',\n // Vibe statuses\n INITIALIZING: 'info',\n INACTIVE: 'neutral',\n ARCHIVED: 'neutral',\n // Port forward statuses\n CONFLICT: 'error',\n // Module statuses\n DRAFT: 'neutral',\n PENDING_REVIEW: 'info',\n APPROVED: 'success',\n REJECTED: 'error',\n DEPRECATED: 'warning',\n // Installation statuses\n INSTALLING: 'info',\n UNINSTALLING: 'warning',\n // Connection statuses\n connected: 'success',\n disconnected: 'neutral',\n connecting: 'info',\n error: 'error',\n // Health statuses\n healthy: 'success',\n degraded: 'warning',\n unhealthy: 'error',\n unknown: 'neutral',\n};\n\n// `info` (connecting / installing transient states) reads NEUTRAL rather than\n// off-brand blue — the product brand is purple and loud blue status pills\n// clash with it. Semantic hues stay for success/warning/error; purple is\n// reserved for primary actions, not badges. All use the subtle recipe so they\n// pass AA in light and dark.\nconst variantStyles: Record<StatusVariant, string> = {\n success: 'bg-status-success-bg-subtle text-status-success-text',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text',\n error: 'bg-status-error-bg-subtle text-status-error-text',\n info: 'bg-bg-sunken text-text-secondary',\n neutral: 'bg-bg-sunken text-text-secondary',\n};\n\nconst dotStyles: Record<StatusVariant, string> = {\n success: 'text-status-success-text',\n warning: 'text-status-warning-text',\n error: 'text-status-error-text',\n info: 'text-text-secondary',\n neutral: 'text-text-secondary',\n};\n\nexport function StatusBadge({\n status,\n variant,\n size = 'sm',\n showDot = true,\n pulse = false,\n}: StatusBadgeProps) {\n const resolvedVariant = variant || variantMap[status] || 'neutral';\n const displayStatus = status.replace(/_/g, ' ').toLowerCase();\n\n const sizeStyles = {\n sm: 'text-xs px-2 py-0.5',\n md: 'text-sm px-2.5 py-1',\n };\n\n const dotSize = size === 'sm' ? 'size-2' : 'size-2.5';\n\n // Boff UI 2.0 — live/active/running statuses get a calm reduced-motion-safe\n // opacity glow-pulse on the status dot (no movement). Honour an explicit\n // `pulse` request too. The pulse is purely cosmetic on the dot.\n const isLive = /^(ACTIVE|RUNNING|connected|connecting|STARTING|healthy)$/i.test(status);\n const pulseDot = pulse || isLive;\n\n return (\n <span\n className={`inline-flex items-center gap-1.5 font-medium rounded-full capitalize ${sizeStyles[size]} ${variantStyles[resolvedVariant]}`}\n >\n {showDot && (\n <Circle\n className={`${dotSize} fill-current ${dotStyles[resolvedVariant]} ${pulseDot ? 'motion-safe:animate-vc-glow-pulse' : ''}`}\n />\n )}\n {displayStatus}\n </span>\n );\n}\n"],"mappings":";;;AAYA,IAAM,IAA4C;CAEhD,QAAQ;CACR,SAAS;CACT,MAAM;CACN,aAAa;CACb,UAAU;CACV,SAAS;CACT,SAAS;CAET,SAAS;CACT,UAAU;CACV,UAAU;CACV,SAAS;CACT,OAAO;CACP,YAAY;CAEZ,cAAc;CACd,UAAU;CACV,UAAU;CAEV,UAAU;CAEV,OAAO;CACP,gBAAgB;CAChB,UAAU;CACV,UAAU;CACV,YAAY;CAEZ,YAAY;CACZ,cAAc;CAEd,WAAW;CACX,cAAc;CACd,YAAY;CACZ,OAAO;CAEP,SAAS;CACT,UAAU;CACV,WAAW;CACX,SAAS;CACV,EAOK,IAA+C;CACnD,SAAS;CACT,SAAS;CACT,OAAO;CACP,MAAM;CACN,SAAS;CACV,EAEK,IAA2C;CAC/C,SAAS;CACT,SAAS;CACT,OAAO;CACP,MAAM;CACN,SAAS;CACV;AAED,SAAgB,EAAY,EAC1B,WACA,YACA,UAAO,MACP,aAAU,IACV,WAAQ,MACW;CACnB,IAAM,IAAkB,KAAW,EAAW,MAAW,WACnD,IAAgB,EAAO,QAAQ,MAAM,IAAI,CAAC,aAAa,EAEvD,IAAa;EACjB,IAAI;EACJ,IAAI;EACL,EAEK,IAAU,MAAS,OAAO,WAAW,YAKrC,IAAS,4DAA4D,KAAK,EAAO,EACjF,IAAW,KAAS;AAE1B,QACE,kBAAC,QAAD;EACE,WAAW,wEAAwE,EAAW,GAAM,GAAG,EAAc;YADvH,CAGG,KACC,kBAAC,GAAD,EACE,WAAW,GAAG,EAAQ,gBAAgB,EAAU,GAAiB,GAAG,IAAW,sCAAsC,MACrH,CAAA,EAEH,EACI"}
@@ -635,6 +635,7 @@ function at() {
635
635
  }, "list-view")
636
636
  }) : /* @__PURE__ */ m(Se, {
637
637
  icon: /* @__PURE__ */ m(Ie, { className: "size-8 text-text-secondary" }),
638
+ illustration: x || S !== "all" || C !== "all" ? "empty-search" : "empty-agents",
638
639
  title: x || S !== "all" || C !== "all" ? b("agentsPage.emptyNoAgentsFound", "No agents found") : b("agentsPage.emptyNoAgentsConfigured", "No agents configured"),
639
640
  description: x || S !== "all" || C !== "all" ? b("agentsPage.emptyAdjustSearch", "Try adjusting your search or filters") : b("agentsPage.emptyNoAgentsDescription", "Install the CLI agent on your machine, then add it here. You can create a target inline while adding the agent."),
640
641
  action: !x && S === "all" && C === "all" ? /* @__PURE__ */ h("div", {