cli-jaw 2.2.25 → 2.3.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.
Files changed (159) hide show
  1. package/README.md +5 -2
  2. package/dist/bin/cli-jaw.js +6 -1
  3. package/dist/bin/cli-jaw.js.map +1 -1
  4. package/dist/bin/commands/doctor.js +39 -18
  5. package/dist/bin/commands/doctor.js.map +1 -1
  6. package/dist/bin/commands/init.js +82 -23
  7. package/dist/bin/commands/init.js.map +1 -1
  8. package/dist/bin/commands/messaging.js +262 -0
  9. package/dist/bin/commands/messaging.js.map +1 -0
  10. package/dist/server.js +30 -6
  11. package/dist/server.js.map +1 -1
  12. package/dist/src/cli/commands.js +6 -2
  13. package/dist/src/cli/commands.js.map +1 -1
  14. package/dist/src/cli/handlers/remote-session-commands.js +94 -0
  15. package/dist/src/cli/handlers/remote-session-commands.js.map +1 -0
  16. package/dist/src/command-contract/catalog.js +1 -1
  17. package/dist/src/command-contract/catalog.js.map +1 -1
  18. package/dist/src/core/chat-sessions.js +4 -0
  19. package/dist/src/core/chat-sessions.js.map +1 -1
  20. package/dist/src/core/config.js +63 -22
  21. package/dist/src/core/config.js.map +1 -1
  22. package/dist/src/core/db.js +6 -0
  23. package/dist/src/core/db.js.map +1 -1
  24. package/dist/src/core/dispatch-approval-ingress.js +45 -3
  25. package/dist/src/core/dispatch-approval-ingress.js.map +1 -1
  26. package/dist/src/core/dispatch-approval.js +46 -0
  27. package/dist/src/core/dispatch-approval.js.map +1 -1
  28. package/dist/src/core/logger.js +67 -0
  29. package/dist/src/core/logger.js.map +1 -1
  30. package/dist/src/core/session-generation.js +74 -0
  31. package/dist/src/core/session-generation.js.map +1 -0
  32. package/dist/src/discord/bot.js +91 -5
  33. package/dist/src/discord/bot.js.map +1 -1
  34. package/dist/src/discord/commands.js +34 -1
  35. package/dist/src/discord/commands.js.map +1 -1
  36. package/dist/src/discord/forwarder.js +4 -1
  37. package/dist/src/discord/forwarder.js.map +1 -1
  38. package/dist/src/discord/send-only-client.js +14 -7
  39. package/dist/src/discord/send-only-client.js.map +1 -1
  40. package/dist/src/messaging/access-policy.js +26 -0
  41. package/dist/src/messaging/access-policy.js.map +1 -0
  42. package/dist/src/messaging/approval-presentation.js +89 -0
  43. package/dist/src/messaging/approval-presentation.js.map +1 -0
  44. package/dist/src/messaging/channel-adapter.js +47 -0
  45. package/dist/src/messaging/channel-adapter.js.map +1 -0
  46. package/dist/src/messaging/channel-capabilities.js +47 -15
  47. package/dist/src/messaging/channel-capabilities.js.map +1 -1
  48. package/dist/src/messaging/channel-health.js +23 -3
  49. package/dist/src/messaging/channel-health.js.map +1 -1
  50. package/dist/src/messaging/delivery-outcome.js +12 -0
  51. package/dist/src/messaging/delivery-outcome.js.map +1 -1
  52. package/dist/src/messaging/durable-ingress.js +458 -0
  53. package/dist/src/messaging/durable-ingress.js.map +1 -0
  54. package/dist/src/messaging/effect-once.js +204 -0
  55. package/dist/src/messaging/effect-once.js.map +1 -0
  56. package/dist/src/messaging/inbound-envelope.js +168 -0
  57. package/dist/src/messaging/inbound-envelope.js.map +1 -0
  58. package/dist/src/messaging/ingress-audit.js +41 -0
  59. package/dist/src/messaging/ingress-audit.js.map +1 -0
  60. package/dist/src/messaging/ingress-generation.js +22 -0
  61. package/dist/src/messaging/ingress-generation.js.map +1 -0
  62. package/dist/src/messaging/metrics.js +69 -0
  63. package/dist/src/messaging/metrics.js.map +1 -0
  64. package/dist/src/messaging/outbound-outbox.js +184 -0
  65. package/dist/src/messaging/outbound-outbox.js.map +1 -0
  66. package/dist/src/messaging/remote-command-context.js +30 -0
  67. package/dist/src/messaging/remote-command-context.js.map +1 -0
  68. package/dist/src/messaging/retry.js +11 -2
  69. package/dist/src/messaging/retry.js.map +1 -1
  70. package/dist/src/messaging/runtime.js +129 -30
  71. package/dist/src/messaging/runtime.js.map +1 -1
  72. package/dist/src/messaging/send.js +31 -6
  73. package/dist/src/messaging/send.js.map +1 -1
  74. package/dist/src/messaging/trace-context.js +56 -0
  75. package/dist/src/messaging/trace-context.js.map +1 -0
  76. package/dist/src/messaging/types.js +39 -1
  77. package/dist/src/messaging/types.js.map +1 -1
  78. package/dist/src/routes/orchestrate.js +10 -3
  79. package/dist/src/routes/orchestrate.js.map +1 -1
  80. package/dist/src/routes/settings.js +26 -1
  81. package/dist/src/routes/settings.js.map +1 -1
  82. package/dist/src/slack/bot.js +182 -70
  83. package/dist/src/slack/bot.js.map +1 -1
  84. package/dist/src/slack/commands.js +16 -1
  85. package/dist/src/slack/commands.js.map +1 -1
  86. package/dist/src/slack/format.js +4 -1
  87. package/dist/src/slack/format.js.map +1 -1
  88. package/dist/src/slack/manifest.js +5 -0
  89. package/dist/src/slack/manifest.js.map +1 -1
  90. package/dist/src/slack/send-handler.js +26 -4
  91. package/dist/src/slack/send-handler.js.map +1 -1
  92. package/dist/src/slack/send-only-client.js +26 -2
  93. package/dist/src/slack/send-only-client.js.map +1 -1
  94. package/dist/src/slack/socket.js +36 -13
  95. package/dist/src/slack/socket.js.map +1 -1
  96. package/dist/src/telegram/bot.js +122 -14
  97. package/dist/src/telegram/bot.js.map +1 -1
  98. package/package.json +4 -1
  99. package/public/dist/assets/ChannelEnablementControl-Da_t_jq6.js +1 -0
  100. package/public/dist/assets/ChannelsDiscord-kNjCCJcA.js +1 -0
  101. package/public/dist/assets/ChannelsSlack-BOuoCfeN.js +1 -0
  102. package/public/dist/assets/ChannelsTelegram-DBIFDz4R.js +1 -0
  103. package/public/dist/assets/{app-DUbf-v7B.js → app-TWwXw3iD.js} +4 -4
  104. package/public/dist/assets/bgtask-badge-BhzSqQHG.js +1 -0
  105. package/public/dist/assets/{bgtask-badge-jIH69Dp3.js → bgtask-badge-DaAUnoKD.js} +1 -1
  106. package/public/dist/assets/{employees-BNy8a6XX.js → employees-BFKkaVAe.js} +1 -1
  107. package/public/dist/assets/iframe-renderer-BxEzdKIA.js +1 -0
  108. package/public/dist/assets/{iframe-renderer-C6Zwg9w4.js → iframe-renderer-CvnKi77F.js} +2 -2
  109. package/public/dist/assets/{manager-DzCR6Pam.js → manager-BTpvAFwX.js} +1 -1
  110. package/public/dist/assets/memory-DHOtCiw5.js +1 -0
  111. package/public/dist/assets/{memory-C4O44HMr.js → memory-db_s-38T.js} +1 -1
  112. package/public/dist/assets/{message-history-DCxD21J1.js → message-history-BX9shNKA.js} +1 -1
  113. package/public/dist/assets/message-history-DaSxkyy0.js +1 -0
  114. package/public/dist/assets/{render-A--hkukv.js → render-DZnFK8Kv.js} +1 -1
  115. package/public/dist/assets/{settings-3ZdoKHIf.js → settings-BuFMKcQ-.js} +1 -1
  116. package/public/dist/assets/settings-DotRDITm.js +1 -0
  117. package/public/dist/assets/{settings-core-CQe1uP6x.js → settings-core-3fj0DVoa.js} +17 -17
  118. package/public/dist/assets/settings-core-DohSnMad.js +1 -0
  119. package/public/dist/assets/{sidebar-DxFp_449.js → sidebar-D5TP7TCO.js} +3 -3
  120. package/public/dist/assets/{skills-BU4q8Q8n.js → skills-CQzHFYJS.js} +1 -1
  121. package/public/dist/assets/skills-DZHEvl1x.js +1 -0
  122. package/public/dist/assets/{slash-commands-BiXOgZ5B.js → slash-commands-BWEWWf-c.js} +1 -1
  123. package/public/dist/assets/slash-commands-CuJdloNC.js +1 -0
  124. package/public/dist/assets/{trace-drawer-2BTQCor6.js → trace-drawer-CBR-PZB4.js} +1 -1
  125. package/public/dist/assets/ui-CWl27eMl.js +1 -0
  126. package/public/dist/assets/{ui-Csy42uNW.js → ui-Ca1Q-VFQ.js} +1 -1
  127. package/public/dist/index.html +1 -1
  128. package/public/dist/manager/index.html +1 -1
  129. package/public/js/features/settings-channel.ts +35 -5
  130. package/public/js/features/settings-types.ts +7 -0
  131. package/public/js/features/transport-status-row.ts +23 -4
  132. package/public/manager/src/settings/pages/ChannelsDiscord.tsx +39 -11
  133. package/public/manager/src/settings/pages/ChannelsSlack.tsx +24 -7
  134. package/public/manager/src/settings/pages/ChannelsTelegram.tsx +38 -10
  135. package/public/manager/src/settings/pages/components/ChannelEnablementControl.tsx +98 -0
  136. package/public/manager/src/settings/pages/components/TransportStatusChips.tsx +23 -2
  137. package/scripts/docs/extract-commands.mts +3 -0
  138. package/scripts/generate-channel-capability-table.mts +161 -0
  139. package/scripts/promote-to-main.sh +175 -0
  140. package/scripts/release-gates.mjs +57 -3
  141. package/scripts/release-preview.sh +19 -0
  142. package/scripts/require-release-evidence.mjs +35 -1
  143. package/scripts/validate-messaging-certification.mts +83 -0
  144. package/scripts/write-messaging-certification.mts +65 -0
  145. package/public/dist/assets/ChannelsDiscord-D4fD-9hP.js +0 -1
  146. package/public/dist/assets/ChannelsSlack-_KBwRonM.js +0 -1
  147. package/public/dist/assets/ChannelsTelegram-tihzSWVE.js +0 -1
  148. package/public/dist/assets/TransportStatusChips-B0NR0ljO.js +0 -1
  149. package/public/dist/assets/bgtask-badge-B8KhuBa7.js +0 -1
  150. package/public/dist/assets/iframe-renderer-DBmV8gCQ.js +0 -1
  151. package/public/dist/assets/memory-Dt_Ji6aP.js +0 -1
  152. package/public/dist/assets/message-history-DyISauiv.js +0 -1
  153. package/public/dist/assets/settings-C6Pku2GC.js +0 -1
  154. package/public/dist/assets/settings-core-DqwLJkgP.js +0 -1
  155. package/public/dist/assets/skills-x2gH-1Ry.js +0 -1
  156. package/public/dist/assets/slash-commands-s8eR7Kd1.js +0 -1
  157. package/public/dist/assets/ui-CB0L33o0.js +0 -1
  158. package/scripts/release.sh +0 -224
  159. /package/public/dist/assets/{HealthBadge-CUaJ1PQH.js → HealthBadge-CiNMS-1k.js} +0 -0
@@ -1,2 +1,2 @@
1
1
  const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/vendor-render-Bjnw0wQ6.css"])))=>i.map(i=>d[i]);
2
- import{t as e}from"./state-DxpgDKMO.js";import{t}from"./preload-helper-DFYRRz9w.js";import{N as n,T as r,j as i,k as a,t as o}from"./render-A--hkukv.js";import{A as s,C as c,D as l,N as u,O as d,S as f,_ as ee,b as te,k as p,m,t as ne,v as h,w as g,x as re}from"./iframe-renderer-C6Zwg9w4.js";import{C as _,D as v,E as y,S as b,_ as x,b as S,f as ie,g as C,h as w,l as T,m as E,p as D,u as ae,v as oe,w as se,x as O,y as k}from"./message-history-DCxD21J1.js";import{s as ce}from"./idb-cache-lOntwU3S.js";var A=2e3,j=80,le=400;function ue(e){if(e<=A)return j;let t=e/A*j;return Math.min(le,Math.round(t))}function M(e){return{chunks:[],fullText:``,textDirty:!1,element:e,pendingRAF:null,isFinalized:!1,lastRenderTime:0}}function N(e,t=``){let n=M(e);return t?(n.chunks=[t],n.fullText=t,n.textDirty=!1,n.element.innerHTML=o(t,!0)+`<span class="stream-cursor" aria-hidden="true"></span>`):n.element.innerHTML=`<span class="stream-cursor" aria-hidden="true"></span>`,n}function P(e){return e.textDirty&&=(e.fullText=e.chunks.join(``),!1),e.fullText}function F(e,t){e.chunks.push(t),e.textDirty=!0,!e.pendingRAF&&!e.isFinalized&&(e.pendingRAF=requestAnimationFrame(()=>{if(e.pendingRAF=null,e.isFinalized)return;let t=performance.now(),n=P(e);n.length<A||t-e.lastRenderTime>ue(n.length)?(e.element.innerHTML=o(n,!0)+`<span class="stream-cursor" aria-hidden="true"></span>`,e.lastRenderTime=t):e.pendingRAF=requestAnimationFrame(()=>{e.pendingRAF=null,!e.isFinalized&&(e.element.innerHTML=o(P(e),!0)+`<span class="stream-cursor" aria-hidden="true"></span>`,e.lastRenderTime=performance.now())})}))}function I(e,t=!1){e.isFinalized=!0,e.pendingRAF&&=(cancelAnimationFrame(e.pendingRAF),null);let n=P(e);return t||(e.element.innerHTML=o(n)),n}function L(e){return typeof e.traceSeq==`number`&&Number.isFinite(e.traceSeq)&&e.traceSeq>0}function R(e,t){return!!t.traceRunId&&!!e.traceRunId&&t.traceRunId===e.traceRunId&&L(t)&&L(e)&&t.traceSeq===e.traceSeq&&t.type===e.type&&t.label===e.label&&!!t.isEmployee==!!e.isEmployee}function z(e,t){return t.stepRef&&e.stepRef===t.stepRef?!0:R(e,t)}function B(e,t,n={}){return[...e].reverse().filter(e=>n.includeDone||e.status===`running`).find(e=>z(e,t))??null}function V(e,t){let n=e.filter(e=>e.status===`running`&&!e.stepRef&&e.label===t.label&&e.type===t.type&&!!e.isEmployee==!!t.isEmployee);return n.length===1?n[0]:null}function H(e,t){let n=[...e].reverse().filter(e=>e.status===`running`);return B(n,t)||V(n,t)}function U(){e.currentProcessBlock&&l(e.currentProcessBlock),e.currentAgentDiv instanceof HTMLElement&&l(e.currentAgentDiv),v(),s(),e.currentAgentDiv=null,e.currentProcessBlock=null,q=null}var W=0;function de(){W=Date.now()}function fe(){W=0}function pe(){return Date.now()-W<8e3}function G(e){for(let t of document.querySelectorAll(`.msg-user`))if(e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING)return!0;return!1}function K(){let t=e.currentAgentDiv&&e.currentAgentDiv.isConnected?e.currentAgentDiv:null;return t?G(t)?(e.currentAgentDiv=null,e.currentProcessBlock=null,q=null,null):t:null}function me(){let e=Array.from(document.querySelectorAll(`.msg-agent`)).at(-1)??null;return!e||G(e)?null:e}function he(t,n){k();let r=K();if(r||(r=x(`agent`,``),e.currentAgentDiv=r,e.currentProcessBlock=null),b(r),!e.currentProcessBlock){let t=r.querySelector(`.agent-body`);t&&(e.currentProcessBlock=S(r)),!e.currentProcessBlock&&t&&(_(r),e.currentProcessBlock=f(t))}if(n&&e.currentProcessBlock&&!e.currentProcessBlock.startedAt&&(e.currentProcessBlock.startedAt=n),e.currentProcessBlock){let n=t.rawIcon||t.icon,r=t.status||a(t.icon)||`running`;if(r===`done`||r===`error`){let a=H(e.currentProcessBlock.steps,t);if(a){t.icon=i(t.icon);let o=t.type===`thinking`?p(a.id,t.detail):g(a.id,t.detail);d(e.currentProcessBlock,a.id,{...a,...t,id:a.id,rawIcon:n,detail:o,detailPreview:o,label:t.label||a.label,status:r,startTime:a.startTime}),m();return}if(t.stepRef&&(r===`done`||r===`error`)){let a=[...e.currentProcessBlock.steps].reverse().find(e=>e.stepRef===t.stepRef&&(e.status===`done`||e.status===`error`));if(a){t.icon=i(t.icon);let o=t.type===`thinking`?p(a.id,t.detail):g(a.id,t.detail);d(e.currentProcessBlock,a.id,{...a,...t,id:a.id,rawIcon:n,status:r,detail:o,detailPreview:o,startTime:a.startTime}),m();return}}}if(t.stepRef&&r===`running`){let r=[...e.currentProcessBlock.steps].reverse().find(e=>e.stepRef===t.stepRef&&e.status===`running`);if(r){t.rawIcon=n,t.icon=i(t.icon);let a=p(r.id,t.detail);d(e.currentProcessBlock,r.id,{...r,...t,id:r.id,rawIcon:n,status:`running`,detail:a,detailPreview:a,label:t.label||r.label,startTime:r.startTime}),m();return}}let o=B(e.currentProcessBlock.steps,t,{includeDone:!0});if(o){t.rawIcon=n,t.icon=i(t.icon);let a=t.detail||``,s=c(o.id)||o.detail||o.detailPreview||``,l=a.length>=s.length?a:s,u=p(o.id,l),f=(o.status===`done`||o.status===`error`)&&r===`running`?o.status:r;d(e.currentProcessBlock,o.id,{...o,...t,id:o.id,rawIcon:n,status:f,detail:u,detailPreview:u,label:t.label||o.label,startTime:o.startTime}),m();return}if(t.detail){let n=[...e.currentProcessBlock.steps].reverse().find(e=>e.status===`running`&&e.label===t.label&&e.type===t.type&&!!e.isEmployee==!!t.isEmployee&&!e.detail);if(n){d(e.currentProcessBlock,n.id,t),m();return}}t.rawIcon=n,t.icon=i(t.icon),h(e.currentProcessBlock,t)}m()}var q=null,J=`data-active-run-hydrated`;function Y(e){document.querySelectorAll(`[${J}="true"]`).forEach(t=>{e&&t===e||t.remove()})}function ge(e){let t=K()??me();Y(t);let n=t||x(`agent`,``,e||null);return n.setAttribute(J,`true`),n}function _e(e,t){let n=c(e.id)||e.detail||e.detailPreview||``,r=t.detail||t.detailPreview||``;return r.length>=n.length?r:n}function X(e){return e.stepRef?!0:!!e.traceRunId&&typeof e.traceSeq==`number`&&Number.isFinite(e.traceSeq)&&e.traceSeq>0}function ve(e,t){let n=e.steps.filter(e=>!X(e)),r=0,i=!0;for(let a of t){let t=e.steps.find(e=>z(e,a))??null;if(!t&&!X(a)&&i){let e=n[r]??null;e?e.type===a.type&&!!e.isEmployee==!!a.isEmployee?(t=e,r++):i=!1:r++}if(!t){let n=e.steps.filter(e=>e.label===a.label&&e.type===a.type&&!!e.isEmployee==!!a.isEmployee);t=n.length===1?n[0]:null}if(!t){if(h(e,a),!X(a)&&i){let t=e.steps[e.steps.length-1];t&&n.push(t)}continue}let o=p(t.id,_e(t,a));d(e,t.id,{...t,...a,id:t.id,detail:o,detailPreview:o,label:a.label||t.label})}}function ye(e=[]){document.querySelectorAll(`[data-queued-overlay="true"]`).forEach(e=>e.remove())}var Z=``;function be(e){let t=Array.isArray(e.toolLog)?e.toolLog:[],n=t.map(e=>`${e.stepRef||``}~${e.traceRunId||``}~${e.traceSeq??``}~${e.status||``}~${e.label||``}~${(e.detail||``).length}`).join(`|`);return`${e.traceRunId||``}:${e.cli||``}:${(e.text||``).length}:${t.length}:${n}`}function xe(t){if(!t?.running){Z=``,Y();return}let n=be(t);if(n===Z&&K())return;v(),k(),e.currentAgentDiv=ge(t.cli||null);let r=e.currentAgentDiv.querySelector(`.agent-body`),i=E(t.toolLog||[]);b(e.currentAgentDiv),e.currentProcessBlock=S(e.currentAgentDiv),r&&i.length?(e.currentProcessBlock||=(_(e.currentAgentDiv),f(r)),t.startedAt&&(e.currentProcessBlock.startedAt=t.startedAt),ve(e.currentProcessBlock,C(i,t.startedAt))):e.currentProcessBlock=S(e.currentAgentDiv);let a=e.currentAgentDiv.querySelector(`.msg-content`);a&&(q=N(a,t.text||``)),Z=n}function Q(t){if(!t)return;k(),K()||(e.currentAgentDiv=x(`agent`,``),q=null);let n=e.currentAgentDiv?.querySelector(`.msg-content`);n&&(q||=M(n),F(q,t)),m()}var $=0;function Se(e){e.querySelectorAll(`.mermaid-pending`).forEach(e=>{delete e.dataset.mermaidQueued,delete e.dataset.mermaidQueuedAt})}function Ce(t,i){let a=Date.now();if(!e.currentAgentDiv&&a-$<500)return;Z=``,v(),k(),e.currentAgentDiv&&b(e.currentAgentDiv);let s=se(e.currentProcessBlock??e.currentAgentDiv,!0),c=E(D(Array.isArray(i)?i:[],s)),d=w(c),f=!!e.currentProcessBlock||!!(e.currentAgentDiv&&O(e.currentAgentDiv));e.currentProcessBlock&&=(re(e.currentProcessBlock),null);let p=c.length>0;if(t||p){(!e.currentAgentDiv||!e.currentAgentDiv.isConnected)&&(e.currentAgentDiv=x(`agent`,``)),e.currentAgentDiv.removeAttribute(J);let i=e.currentAgentDiv?.querySelector(`.msg-content`),a=q?I(q,!0):``,s=t||a;if(q=null,i&&(i.innerHTML=o(s)),p&&e.currentAgentDiv&&!f&&!O(e.currentAgentDiv)){let t=e.currentAgentDiv.querySelector(`.msg-content`);t&&t.insertAdjacentHTML(`beforebegin`,te(C(c),!0))}e.currentAgentDiv&&b(e.currentAgentDiv),i&&i.setAttribute(`data-raw`,n(s)),i&&ne(i);let m=ee(),h=!!(m.active&&e.currentAgentDiv&&e.currentAgentDiv.isConnected);if(i&&!h&&r(i,{immediate:!0}),h){let t=e.currentAgentDiv;Se(t),t.querySelectorAll(`.diagram-widget`).forEach(e=>{let t=e,n=t.dataset.widgetHtml,r=t.dataset.widgetId;if(!n&&!r)return;let i=document.createElement(`div`);i.className=`diagram-widget-pending`,r?i.dataset.widgetId=r:n&&(i.dataset.diagramHtml=n),e.replaceWith(i)}),d?(m.appendItem(ie({role:`assistant`,content:s,cli:null,tool_log:d},m.count)),l(t),m.scrollToBottom()):(t.dataset.turnIndex=String(m.count),t.dataset.messageId||(t.dataset.messageId=u()),m.appendLiveItem(t)),t.remove()}s&&ce({role:`assistant`,content:s,tool_log:d,timestamp:Date.now()}).catch(()=>{})}q=null,e.currentAgentDiv=null,$=Date.now(),ae(`idle`),T()}function we(e,n){document.querySelectorAll(`.tab-btn`).forEach(e=>{e.classList.remove(`active`),e.setAttribute(`aria-selected`,`false`)}),document.querySelectorAll(`.tab-content`).forEach(e=>e.classList.remove(`active`)),document.getElementById({agents:`tabAgents`,settings:`tabSettings`,skills:`tabSkills`}[e])?.classList.add(`active`),n&&(n.classList.add(`active`),n.setAttribute(`aria-selected`,`true`)),e===`settings`&&t(()=>import(`./settings-C6Pku2GC.js`).then(e=>e.loadSettings()),__vite__mapDeps([0])),e===`agents`&&t(()=>import(`./employees-BNy8a6XX.js`).then(e=>e.loadEmployees()),__vite__mapDeps([0])),e===`skills`&&t(()=>import(`./skills-x2gH-1Ry.js`).then(e=>e.loadSkills()),__vite__mapDeps([0]))}function Te(){let e=document.getElementById(`tabSettings`)?.classList.contains(`active`);t(()=>import(`./settings-C6Pku2GC.js`).then(t=>e?t.savePerCli():t.updateSettings()),__vite__mapDeps([0]))}function Ee(){y({onStatus:oe})}export{Ce as a,Ee as c,he as d,we as f,fe as i,pe as l,ye as n,Te as o,U as r,xe as s,Q as t,de as u};
2
+ import{t as e}from"./state-DxpgDKMO.js";import{t}from"./preload-helper-DFYRRz9w.js";import{N as n,T as r,j as i,k as a,t as o}from"./render-DZnFK8Kv.js";import{A as s,C as c,D as l,N as u,O as d,S as f,_ as ee,b as te,k as p,m,t as ne,v as h,w as g,x as re}from"./iframe-renderer-CvnKi77F.js";import{C as _,D as v,E as y,S as b,_ as x,b as S,f as ie,g as C,h as w,l as T,m as E,p as D,u as ae,v as oe,w as se,x as O,y as k}from"./message-history-BX9shNKA.js";import{s as ce}from"./idb-cache-lOntwU3S.js";var A=2e3,j=80,le=400;function ue(e){if(e<=A)return j;let t=e/A*j;return Math.min(le,Math.round(t))}function M(e){return{chunks:[],fullText:``,textDirty:!1,element:e,pendingRAF:null,isFinalized:!1,lastRenderTime:0}}function N(e,t=``){let n=M(e);return t?(n.chunks=[t],n.fullText=t,n.textDirty=!1,n.element.innerHTML=o(t,!0)+`<span class="stream-cursor" aria-hidden="true"></span>`):n.element.innerHTML=`<span class="stream-cursor" aria-hidden="true"></span>`,n}function P(e){return e.textDirty&&=(e.fullText=e.chunks.join(``),!1),e.fullText}function F(e,t){e.chunks.push(t),e.textDirty=!0,!e.pendingRAF&&!e.isFinalized&&(e.pendingRAF=requestAnimationFrame(()=>{if(e.pendingRAF=null,e.isFinalized)return;let t=performance.now(),n=P(e);n.length<A||t-e.lastRenderTime>ue(n.length)?(e.element.innerHTML=o(n,!0)+`<span class="stream-cursor" aria-hidden="true"></span>`,e.lastRenderTime=t):e.pendingRAF=requestAnimationFrame(()=>{e.pendingRAF=null,!e.isFinalized&&(e.element.innerHTML=o(P(e),!0)+`<span class="stream-cursor" aria-hidden="true"></span>`,e.lastRenderTime=performance.now())})}))}function I(e,t=!1){e.isFinalized=!0,e.pendingRAF&&=(cancelAnimationFrame(e.pendingRAF),null);let n=P(e);return t||(e.element.innerHTML=o(n)),n}function L(e){return typeof e.traceSeq==`number`&&Number.isFinite(e.traceSeq)&&e.traceSeq>0}function R(e,t){return!!t.traceRunId&&!!e.traceRunId&&t.traceRunId===e.traceRunId&&L(t)&&L(e)&&t.traceSeq===e.traceSeq&&t.type===e.type&&t.label===e.label&&!!t.isEmployee==!!e.isEmployee}function z(e,t){return t.stepRef&&e.stepRef===t.stepRef?!0:R(e,t)}function B(e,t,n={}){return[...e].reverse().filter(e=>n.includeDone||e.status===`running`).find(e=>z(e,t))??null}function V(e,t){let n=e.filter(e=>e.status===`running`&&!e.stepRef&&e.label===t.label&&e.type===t.type&&!!e.isEmployee==!!t.isEmployee);return n.length===1?n[0]:null}function H(e,t){let n=[...e].reverse().filter(e=>e.status===`running`);return B(n,t)||V(n,t)}function U(){e.currentProcessBlock&&l(e.currentProcessBlock),e.currentAgentDiv instanceof HTMLElement&&l(e.currentAgentDiv),v(),s(),e.currentAgentDiv=null,e.currentProcessBlock=null,q=null}var W=0;function de(){W=Date.now()}function fe(){W=0}function pe(){return Date.now()-W<8e3}function G(e){for(let t of document.querySelectorAll(`.msg-user`))if(e.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_FOLLOWING)return!0;return!1}function K(){let t=e.currentAgentDiv&&e.currentAgentDiv.isConnected?e.currentAgentDiv:null;return t?G(t)?(e.currentAgentDiv=null,e.currentProcessBlock=null,q=null,null):t:null}function me(){let e=Array.from(document.querySelectorAll(`.msg-agent`)).at(-1)??null;return!e||G(e)?null:e}function he(t,n){k();let r=K();if(r||(r=x(`agent`,``),e.currentAgentDiv=r,e.currentProcessBlock=null),b(r),!e.currentProcessBlock){let t=r.querySelector(`.agent-body`);t&&(e.currentProcessBlock=S(r)),!e.currentProcessBlock&&t&&(_(r),e.currentProcessBlock=f(t))}if(n&&e.currentProcessBlock&&!e.currentProcessBlock.startedAt&&(e.currentProcessBlock.startedAt=n),e.currentProcessBlock){let n=t.rawIcon||t.icon,r=t.status||a(t.icon)||`running`;if(r===`done`||r===`error`){let a=H(e.currentProcessBlock.steps,t);if(a){t.icon=i(t.icon);let o=t.type===`thinking`?p(a.id,t.detail):g(a.id,t.detail);d(e.currentProcessBlock,a.id,{...a,...t,id:a.id,rawIcon:n,detail:o,detailPreview:o,label:t.label||a.label,status:r,startTime:a.startTime}),m();return}if(t.stepRef&&(r===`done`||r===`error`)){let a=[...e.currentProcessBlock.steps].reverse().find(e=>e.stepRef===t.stepRef&&(e.status===`done`||e.status===`error`));if(a){t.icon=i(t.icon);let o=t.type===`thinking`?p(a.id,t.detail):g(a.id,t.detail);d(e.currentProcessBlock,a.id,{...a,...t,id:a.id,rawIcon:n,status:r,detail:o,detailPreview:o,startTime:a.startTime}),m();return}}}if(t.stepRef&&r===`running`){let r=[...e.currentProcessBlock.steps].reverse().find(e=>e.stepRef===t.stepRef&&e.status===`running`);if(r){t.rawIcon=n,t.icon=i(t.icon);let a=p(r.id,t.detail);d(e.currentProcessBlock,r.id,{...r,...t,id:r.id,rawIcon:n,status:`running`,detail:a,detailPreview:a,label:t.label||r.label,startTime:r.startTime}),m();return}}let o=B(e.currentProcessBlock.steps,t,{includeDone:!0});if(o){t.rawIcon=n,t.icon=i(t.icon);let a=t.detail||``,s=c(o.id)||o.detail||o.detailPreview||``,l=a.length>=s.length?a:s,u=p(o.id,l),f=(o.status===`done`||o.status===`error`)&&r===`running`?o.status:r;d(e.currentProcessBlock,o.id,{...o,...t,id:o.id,rawIcon:n,status:f,detail:u,detailPreview:u,label:t.label||o.label,startTime:o.startTime}),m();return}if(t.detail){let n=[...e.currentProcessBlock.steps].reverse().find(e=>e.status===`running`&&e.label===t.label&&e.type===t.type&&!!e.isEmployee==!!t.isEmployee&&!e.detail);if(n){d(e.currentProcessBlock,n.id,t),m();return}}t.rawIcon=n,t.icon=i(t.icon),h(e.currentProcessBlock,t)}m()}var q=null,J=`data-active-run-hydrated`;function Y(e){document.querySelectorAll(`[${J}="true"]`).forEach(t=>{e&&t===e||t.remove()})}function ge(e){let t=K()??me();Y(t);let n=t||x(`agent`,``,e||null);return n.setAttribute(J,`true`),n}function _e(e,t){let n=c(e.id)||e.detail||e.detailPreview||``,r=t.detail||t.detailPreview||``;return r.length>=n.length?r:n}function X(e){return e.stepRef?!0:!!e.traceRunId&&typeof e.traceSeq==`number`&&Number.isFinite(e.traceSeq)&&e.traceSeq>0}function ve(e,t){let n=e.steps.filter(e=>!X(e)),r=0,i=!0;for(let a of t){let t=e.steps.find(e=>z(e,a))??null;if(!t&&!X(a)&&i){let e=n[r]??null;e?e.type===a.type&&!!e.isEmployee==!!a.isEmployee?(t=e,r++):i=!1:r++}if(!t){let n=e.steps.filter(e=>e.label===a.label&&e.type===a.type&&!!e.isEmployee==!!a.isEmployee);t=n.length===1?n[0]:null}if(!t){if(h(e,a),!X(a)&&i){let t=e.steps[e.steps.length-1];t&&n.push(t)}continue}let o=p(t.id,_e(t,a));d(e,t.id,{...t,...a,id:t.id,detail:o,detailPreview:o,label:a.label||t.label})}}function ye(e=[]){document.querySelectorAll(`[data-queued-overlay="true"]`).forEach(e=>e.remove())}var Z=``;function be(e){let t=Array.isArray(e.toolLog)?e.toolLog:[],n=t.map(e=>`${e.stepRef||``}~${e.traceRunId||``}~${e.traceSeq??``}~${e.status||``}~${e.label||``}~${(e.detail||``).length}`).join(`|`);return`${e.traceRunId||``}:${e.cli||``}:${(e.text||``).length}:${t.length}:${n}`}function xe(t){if(!t?.running){Z=``,Y();return}let n=be(t);if(n===Z&&K())return;v(),k(),e.currentAgentDiv=ge(t.cli||null);let r=e.currentAgentDiv.querySelector(`.agent-body`),i=E(t.toolLog||[]);b(e.currentAgentDiv),e.currentProcessBlock=S(e.currentAgentDiv),r&&i.length?(e.currentProcessBlock||=(_(e.currentAgentDiv),f(r)),t.startedAt&&(e.currentProcessBlock.startedAt=t.startedAt),ve(e.currentProcessBlock,C(i,t.startedAt))):e.currentProcessBlock=S(e.currentAgentDiv);let a=e.currentAgentDiv.querySelector(`.msg-content`);a&&(q=N(a,t.text||``)),Z=n}function Q(t){if(!t)return;k(),K()||(e.currentAgentDiv=x(`agent`,``),q=null);let n=e.currentAgentDiv?.querySelector(`.msg-content`);n&&(q||=M(n),F(q,t)),m()}var $=0;function Se(e){e.querySelectorAll(`.mermaid-pending`).forEach(e=>{delete e.dataset.mermaidQueued,delete e.dataset.mermaidQueuedAt})}function Ce(t,i){let a=Date.now();if(!e.currentAgentDiv&&a-$<500)return;Z=``,v(),k(),e.currentAgentDiv&&b(e.currentAgentDiv);let s=se(e.currentProcessBlock??e.currentAgentDiv,!0),c=E(D(Array.isArray(i)?i:[],s)),d=w(c),f=!!e.currentProcessBlock||!!(e.currentAgentDiv&&O(e.currentAgentDiv));e.currentProcessBlock&&=(re(e.currentProcessBlock),null);let p=c.length>0;if(t||p){(!e.currentAgentDiv||!e.currentAgentDiv.isConnected)&&(e.currentAgentDiv=x(`agent`,``)),e.currentAgentDiv.removeAttribute(J);let i=e.currentAgentDiv?.querySelector(`.msg-content`),a=q?I(q,!0):``,s=t||a;if(q=null,i&&(i.innerHTML=o(s)),p&&e.currentAgentDiv&&!f&&!O(e.currentAgentDiv)){let t=e.currentAgentDiv.querySelector(`.msg-content`);t&&t.insertAdjacentHTML(`beforebegin`,te(C(c),!0))}e.currentAgentDiv&&b(e.currentAgentDiv),i&&i.setAttribute(`data-raw`,n(s)),i&&ne(i);let m=ee(),h=!!(m.active&&e.currentAgentDiv&&e.currentAgentDiv.isConnected);if(i&&!h&&r(i,{immediate:!0}),h){let t=e.currentAgentDiv;Se(t),t.querySelectorAll(`.diagram-widget`).forEach(e=>{let t=e,n=t.dataset.widgetHtml,r=t.dataset.widgetId;if(!n&&!r)return;let i=document.createElement(`div`);i.className=`diagram-widget-pending`,r?i.dataset.widgetId=r:n&&(i.dataset.diagramHtml=n),e.replaceWith(i)}),d?(m.appendItem(ie({role:`assistant`,content:s,cli:null,tool_log:d},m.count)),l(t),m.scrollToBottom()):(t.dataset.turnIndex=String(m.count),t.dataset.messageId||(t.dataset.messageId=u()),m.appendLiveItem(t)),t.remove()}s&&ce({role:`assistant`,content:s,tool_log:d,timestamp:Date.now()}).catch(()=>{})}q=null,e.currentAgentDiv=null,$=Date.now(),ae(`idle`),T()}function we(e,n){document.querySelectorAll(`.tab-btn`).forEach(e=>{e.classList.remove(`active`),e.setAttribute(`aria-selected`,`false`)}),document.querySelectorAll(`.tab-content`).forEach(e=>e.classList.remove(`active`)),document.getElementById({agents:`tabAgents`,settings:`tabSettings`,skills:`tabSkills`}[e])?.classList.add(`active`),n&&(n.classList.add(`active`),n.setAttribute(`aria-selected`,`true`)),e===`settings`&&t(()=>import(`./settings-DotRDITm.js`).then(e=>e.loadSettings()),__vite__mapDeps([0])),e===`agents`&&t(()=>import(`./employees-BFKkaVAe.js`).then(e=>e.loadEmployees()),__vite__mapDeps([0])),e===`skills`&&t(()=>import(`./skills-DZHEvl1x.js`).then(e=>e.loadSkills()),__vite__mapDeps([0]))}function Te(){let e=document.getElementById(`tabSettings`)?.classList.contains(`active`);t(()=>import(`./settings-DotRDITm.js`).then(t=>e?t.savePerCli():t.updateSettings()),__vite__mapDeps([0]))}function Ee(){y({onStatus:oe})}export{Ce as a,Ee as c,he as d,we as f,fe as i,pe as l,ye as n,Te as o,U as r,xe as s,Q as t,de as u};
@@ -31,7 +31,7 @@
31
31
  href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;500;600;700&family=Outfit:wght@400;500;600;700&display=swap"
32
32
  rel="stylesheet">
33
33
  <!-- Vite handles module bundling in dev (HMR) and production (build) -->
34
- <script type="module" crossorigin src="/dist/assets/app-DUbf-v7B.js"></script>
34
+ <script type="module" crossorigin src="/dist/assets/app-TWwXw3iD.js"></script>
35
35
  <link rel="stylesheet" crossorigin href="/dist/assets/vendor-render-Bjnw0wQ6.css">
36
36
  <link rel="stylesheet" crossorigin href="/dist/assets/app-DuyLYRC6.css">
37
37
  </head>
@@ -6,7 +6,7 @@
6
6
  <link rel="icon" type="image/png" href="/dist/assets/icon-192-CwXPIYF5.png">
7
7
  <title>Jaw Manager</title>
8
8
  <script src="/manager/theme-boot.js"></script>
9
- <script type="module" crossorigin src="/dist/assets/manager-DzCR6Pam.js"></script>
9
+ <script type="module" crossorigin src="/dist/assets/manager-BTpvAFwX.js"></script>
10
10
  <link rel="stylesheet" crossorigin href="/dist/assets/vendor-render-Bjnw0wQ6.css">
11
11
  <link rel="stylesheet" crossorigin href="/dist/assets/manager-LkGs6qEJ.css">
12
12
  </head>
@@ -8,22 +8,52 @@ import { refreshTransportStatusRow } from './transport-status-row.js';
8
8
  import { openSetupGuideIfUnconfigured } from './channel-setup-guide.js';
9
9
  import type { SettingsData } from './settings-types.js';
10
10
 
11
- export async function setActiveChannel(ch: 'telegram' | 'discord' | 'slack'): Promise<void> {
11
+ export type MessengerChannel = 'telegram' | 'discord' | 'slack';
12
+ const CHANNELS: MessengerChannel[] = ['telegram', 'discord', 'slack'];
13
+
14
+ function readEnabledChannelsFromUi(): MessengerChannel[] {
15
+ // Classic UI does not yet expose a unified enabled-channel list.
16
+ // Returning [] means enabling a channel sends a singleton enabled set,
17
+ // which preserves the legacy single-active-channel behavior until the
18
+ // Classic UI gains per-channel enabled checkboxes.
19
+ return [];
20
+ }
21
+
22
+ export async function setChannelEnabled(ch: MessengerChannel, enabled: boolean): Promise<void> {
23
+ const current = readEnabledChannelsFromUi();
24
+ const enabledChannels = enabled
25
+ ? [...new Set([...current, ch])]
26
+ : current.filter(item => item !== ch);
27
+ await apiJson('/api/settings', 'PUT', { messaging: { enabledChannels } });
28
+ await refreshTransportStatusRow();
29
+ }
30
+
31
+ export async function setHomeChannel(ch: MessengerChannel): Promise<void> {
12
32
  document.getElementById('chTelegram')?.classList.toggle('active', ch === 'telegram');
13
33
  document.getElementById('chDiscord')?.classList.toggle('active', ch === 'discord');
14
34
  document.getElementById('chSlack')?.classList.toggle('active', ch === 'slack');
15
35
  document.getElementById('channelTelegramSettings')?.style.setProperty('display', ch === 'telegram' ? '' : 'none');
16
36
  document.getElementById('channelDiscordSettings')?.style.setProperty('display', ch === 'discord' ? '' : 'none');
17
37
  document.getElementById('channelSlackSettings')?.style.setProperty('display', ch === 'slack' ? '' : 'none');
18
- await apiJson('/api/settings', 'PUT', { channel: ch });
38
+ await apiJson('/api/settings', 'PUT', { messaging: { homeChannel: ch } });
19
39
  await refreshTransportStatusRow();
20
- // Switching to a channel with no credentials leaves a silently dead
21
- // channel — open its setup guide right away.
40
+ }
41
+
42
+ /**
43
+ * @deprecated Use setChannelEnabled + setHomeChannel. Kept temporarily for
44
+ * classic entry points that still bind the channel tabs.
45
+ */
46
+ export async function setActiveChannel(ch: MessengerChannel): Promise<void> {
47
+ await setChannelEnabled(ch, true);
48
+ await setHomeChannel(ch);
22
49
  openSetupGuideIfUnconfigured(ch);
23
50
  }
24
51
 
25
52
  export function loadActiveChannel(s: SettingsData): void {
26
- const ch = s.channel || 'telegram';
53
+ const messaging = s.messaging;
54
+ const ch: MessengerChannel = messaging?.homeChannel && CHANNELS.includes(messaging.homeChannel)
55
+ ? messaging.homeChannel
56
+ : 'telegram';
27
57
  document.getElementById('chTelegram')?.classList.toggle('active', ch === 'telegram');
28
58
  document.getElementById('chDiscord')?.classList.toggle('active', ch === 'discord');
29
59
  document.getElementById('chSlack')?.classList.toggle('active', ch === 'slack');
@@ -115,6 +115,11 @@ export function describeCliProbeAvailability(
115
115
  export function shouldHydrateRuntimeMigrationResponse(status: number): boolean {
116
116
  return status >= 200 && status < 300 || status === 409;
117
117
  }
118
+ export interface MessagingConfig {
119
+ enabledChannels?: ('telegram' | 'discord' | 'slack')[];
120
+ homeChannel?: 'telegram' | 'discord' | 'slack';
121
+ }
122
+
118
123
  export interface SettingsData {
119
124
  cli: string; workingDir: string; permissions: string; locale?: string; showReasoning?: boolean;
120
125
  perCli?: Record<string, PerCliConfig>;
@@ -123,7 +128,9 @@ export interface SettingsData {
123
128
  discord?: DiscordConfig;
124
129
  slack?: SlackConfig;
125
130
  slackEnvironmentVariables?: string[];
131
+ /** @deprecated Use messaging.homeChannel (response alias). */
126
132
  channel?: 'telegram' | 'discord' | 'slack';
133
+ messaging?: MessagingConfig;
127
134
  fallbackOrder?: string[];
128
135
  memory?: { cli?: string };
129
136
  projectDirs?: string[] | null;
@@ -2,6 +2,8 @@ import { api } from '../api.js';
2
2
  import { escapeHtml } from '../render.js';
3
3
  import { t } from './i18n.js';
4
4
 
5
+ export type MessengerChannel = 'telegram' | 'discord' | 'slack';
6
+
5
7
  export type TransportStatus = {
6
8
  configured: boolean;
7
9
  activeInbound: boolean;
@@ -10,7 +12,9 @@ export type TransportStatus = {
10
12
  };
11
13
 
12
14
  export type ChannelHealth = {
13
- activeInbound: 'telegram' | 'discord' | 'slack';
15
+ /** @deprecated Use activeInboundChannels for the actual running set. */
16
+ activeInbound: MessengerChannel;
17
+ activeInboundChannels: MessengerChannel[];
14
18
  telegram: TransportStatus;
15
19
  discord: TransportStatus;
16
20
  slack: TransportStatus;
@@ -24,15 +28,29 @@ function isTransportStatus(value: unknown): value is TransportStatus {
24
28
  && typeof row['sendCapable'] === 'boolean';
25
29
  }
26
30
 
31
+ function isMessengerChannel(value: unknown): value is MessengerChannel {
32
+ return value === 'telegram' || value === 'discord' || value === 'slack';
33
+ }
34
+
27
35
  export function parseChannelHealth(payload: unknown): ChannelHealth | null {
28
36
  if (!payload || typeof payload !== 'object') return null;
29
37
  const channels = (payload as { channels?: unknown }).channels;
30
38
  if (!channels || typeof channels !== 'object') return null;
31
39
  const row = channels as Record<string, unknown>;
32
- // Extract to a local so TypeScript narrows it for the return below; a
33
- // repeated property read is not narrowed.
40
+ const rawActiveChannels = row['activeInboundChannels'];
41
+ let activeInboundChannels: MessengerChannel[] | null;
42
+ if (rawActiveChannels !== undefined) {
43
+ activeInboundChannels = Array.isArray(rawActiveChannels)
44
+ && rawActiveChannels.every(isMessengerChannel)
45
+ ? [...new Set(rawActiveChannels)]
46
+ : null;
47
+ } else {
48
+ const legacyActive = row['activeInbound'];
49
+ activeInboundChannels = isMessengerChannel(legacyActive) ? [legacyActive] : null;
50
+ }
51
+ if (!activeInboundChannels) return null;
34
52
  const active = row['activeInbound'];
35
- if (active !== 'telegram' && active !== 'discord' && active !== 'slack') return null;
53
+ if (!isMessengerChannel(active)) return null;
36
54
  if (!isTransportStatus(row['telegram']) || !isTransportStatus(row['discord'])) return null;
37
55
  // Slack is tolerated as absent: a newer bundle can be served against an
38
56
  // older running server during a rolling update, and rejecting the whole
@@ -42,6 +60,7 @@ export function parseChannelHealth(payload: unknown): ChannelHealth | null {
42
60
  : { configured: false, activeInbound: false, sendCapable: false, reason: 'unavailable' };
43
61
  return {
44
62
  activeInbound: active,
63
+ activeInboundChannels,
45
64
  telegram: row['telegram'],
46
65
  discord: row['discord'],
47
66
  slack,
@@ -1,7 +1,8 @@
1
1
  // Phase 3 — Channels (Discord) page.
2
2
  //
3
3
  // Settings keys:
4
- // channel (shared across telegram + discord pages)
4
+ // messaging.enabledChannels
5
+ // messaging.homeChannel
5
6
  // discord.enabled
6
7
  // discord.token (SecretField, masked, never logged)
7
8
  // discord.guildId
@@ -21,10 +22,15 @@ import {
21
22
  usePageSnapshot,
22
23
  } from './page-shell';
23
24
  import { expandPatch } from './path-utils';
24
- import { ActiveChannelToggle } from './components/ActiveChannelToggle';
25
- import type { ActiveChannel } from './components/ActiveChannelToggle';
26
25
  import { HealthBadge, interpretDiscordHealth } from './components/HealthBadge';
26
+ import type { MessengerChannel } from './components/ChannelEnablementControl';
27
27
  import { TransportStatusChips } from './components/TransportStatusChips';
28
+ import { ChannelEnablementControl } from './components/ChannelEnablementControl';
29
+
30
+ type MessagingBlock = {
31
+ enabledChannels?: MessengerChannel[];
32
+ homeChannel?: MessengerChannel;
33
+ };
28
34
 
29
35
  type DiscordBlock = {
30
36
  enabled?: boolean;
@@ -37,13 +43,15 @@ type DiscordBlock = {
37
43
  };
38
44
 
39
45
  type DiscordSnapshot = {
40
- channel?: ActiveChannel;
46
+ channel?: MessengerChannel;
41
47
  discord?: DiscordBlock;
48
+ messaging?: MessagingBlock;
42
49
  [key: string]: unknown;
43
50
  };
44
51
 
45
52
  const DISCORD_KEYS = [
46
- 'channel',
53
+ 'messaging.enabledChannels',
54
+ 'messaging.homeChannel',
47
55
  'discord.enabled',
48
56
  'discord.token',
49
57
  'discord.guildId',
@@ -62,6 +70,8 @@ export function isValidSnowflake(chip: string): boolean {
62
70
  export default function ChannelsDiscord({ port, client, dirty, registerSave }: SettingsPageProps) {
63
71
  const { state, refresh, setData } = usePageSnapshot<DiscordSnapshot>(client, '/api/settings');
64
72
 
73
+ const [enabledChannels, setEnabledChannels] = useState<MessengerChannel[]>([]);
74
+ const [homeChannel, setHomeChannel] = useState<MessengerChannel>('telegram');
65
75
  const [enabled, setEnabled] = useState(false);
66
76
  const [token, setToken] = useState('');
67
77
  const [guildId, setGuildId] = useState('');
@@ -72,6 +82,10 @@ export default function ChannelsDiscord({ port, client, dirty, registerSave }: S
72
82
 
73
83
  useEffect(() => {
74
84
  if (state.kind !== 'ready') return;
85
+ const messaging = state.data.messaging || {};
86
+ const rawEnabled = Array.isArray(messaging.enabledChannels) ? messaging.enabledChannels : [];
87
+ setEnabledChannels(rawEnabled.filter(isMessengerChannel) as MessengerChannel[]);
88
+ setHomeChannel(isMessengerChannel(messaging.homeChannel) ? messaging.homeChannel : 'telegram');
75
89
  const dc = state.data.discord || {};
76
90
  setEnabled(Boolean(dc.enabled));
77
91
  setToken('');
@@ -98,8 +112,6 @@ export default function ChannelsDiscord({ port, client, dirty, registerSave }: S
98
112
  return state.data.discord || {};
99
113
  }, [state]);
100
114
 
101
- const originalChannel = state.kind === 'ready' ? state.data.channel : undefined;
102
-
103
115
  const onSave = useCallback(async () => {
104
116
  const bundle = dirty.saveBundle();
105
117
  if (Object.keys(bundle).length === 0) return;
@@ -110,6 +122,13 @@ export default function ChannelsDiscord({ port, client, dirty, registerSave }: S
110
122
  : updated) as DiscordSnapshot;
111
123
  dirty.clear();
112
124
  setData(fresh);
125
+ const freshMessaging = fresh.messaging || {};
126
+ setEnabledChannels(
127
+ Array.isArray(freshMessaging.enabledChannels)
128
+ ? freshMessaging.enabledChannels.filter(isMessengerChannel) as MessengerChannel[]
129
+ : [],
130
+ );
131
+ setHomeChannel(isMessengerChannel(freshMessaging.homeChannel) ? freshMessaging.homeChannel : 'telegram');
113
132
  const dc = fresh.discord || {};
114
133
  setEnabled(Boolean(dc.enabled));
115
134
  setToken('');
@@ -151,12 +170,17 @@ export default function ChannelsDiscord({ port, client, dirty, registerSave }: S
151
170
  >
152
171
  <SettingsSection
153
172
  title="Channels"
154
- hint="Choose which channel receives inbound chat. Outbound send can still work on the other channel when configured."
173
+ hint="Choose which channels receive inbound chat. Outbound send can still work on any configured channel."
155
174
  >
156
- <ActiveChannelToggle
157
- original={originalChannel}
175
+ <ChannelEnablementControl
176
+ pageChannel="discord"
177
+ snapshot={state.kind === 'ready' ? state.data : {}}
178
+ enabledChannels={enabledChannels}
179
+ homeChannel={homeChannel}
180
+ setEnabledChannels={setEnabledChannels}
181
+ setHomeChannel={setHomeChannel}
158
182
  dirty={dirty}
159
- idPrefix="dc-channel"
183
+ idPrefix="di-channel"
160
184
  />
161
185
  <TransportStatusChips client={client} channel="discord" />
162
186
  </SettingsSection>
@@ -281,3 +305,7 @@ export default function ChannelsDiscord({ port, client, dirty, registerSave }: S
281
305
  </form>
282
306
  );
283
307
  }
308
+
309
+ function isMessengerChannel(value: unknown): value is MessengerChannel {
310
+ return value === 'telegram' || value === 'discord' || value === 'slack';
311
+ }
@@ -23,9 +23,9 @@ import {
23
23
  usePageSnapshot,
24
24
  } from './page-shell';
25
25
  import { expandPatch } from './path-utils';
26
- import { ActiveChannelToggle } from './components/ActiveChannelToggle';
27
- import type { ActiveChannel } from './components/ActiveChannelToggle';
26
+ import type { MessengerChannel } from './components/ChannelEnablementControl';
28
27
  import { TransportStatusChips } from './components/TransportStatusChips';
28
+ import { ChannelEnablementControl } from './components/ChannelEnablementControl';
29
29
 
30
30
  type SlackBlock = {
31
31
  enabled?: boolean;
@@ -40,7 +40,10 @@ type SlackBlock = {
40
40
  };
41
41
 
42
42
  type SlackSnapshot = {
43
- channel?: ActiveChannel;
43
+ messaging?: {
44
+ enabledChannels?: MessengerChannel[];
45
+ homeChannel?: MessengerChannel;
46
+ };
44
47
  slack?: SlackBlock;
45
48
  slackEnvironmentVariables?: string[];
46
49
  [key: string]: unknown;
@@ -72,6 +75,8 @@ export function isValidSlackConversationId(chip: string): boolean {
72
75
  export default function ChannelsSlack({ port, client, dirty, registerSave }: SettingsPageProps) {
73
76
  const { state, refresh, setData } = usePageSnapshot<SlackSnapshot>(client, '/api/settings');
74
77
 
78
+ const [enabledChannels, setEnabledChannels] = useState<MessengerChannel[]>([]);
79
+ const [homeChannel, setHomeChannel] = useState<MessengerChannel>('telegram');
75
80
  const [enabled, setEnabled] = useState(false);
76
81
  const [botToken, setBotToken] = useState('');
77
82
  const [appToken, setAppToken] = useState('');
@@ -98,6 +103,10 @@ export default function ChannelsSlack({ port, client, dirty, registerSave }: Set
98
103
 
99
104
  useEffect(() => {
100
105
  if (state.kind !== 'ready') return;
106
+ const messaging = state.data.messaging || {};
107
+ const rawEnabled = Array.isArray(messaging.enabledChannels) ? messaging.enabledChannels : [];
108
+ setEnabledChannels(rawEnabled.filter(isMessengerChannel) as MessengerChannel[]);
109
+ setHomeChannel(isMessengerChannel(messaging.homeChannel) ? messaging.homeChannel : 'telegram');
101
110
  applySnapshot(state.data.slack || {});
102
111
  }, [state, applySnapshot]);
103
112
 
@@ -117,7 +126,6 @@ export default function ChannelsSlack({ port, client, dirty, registerSave }: Set
117
126
  return state.data.slack || {};
118
127
  }, [state]);
119
128
 
120
- const originalChannel = state.kind === 'ready' ? state.data.channel : undefined;
121
129
  const environmentVariables = state.kind === 'ready' && Array.isArray(state.data.slackEnvironmentVariables)
122
130
  ? state.data.slackEnvironmentVariables
123
131
  : [];
@@ -178,10 +186,15 @@ export default function ChannelsSlack({ port, client, dirty, registerSave }: Set
178
186
  >
179
187
  <SettingsSection
180
188
  title="Channels"
181
- hint="Choose which channel receives inbound chat. Outbound send can still work on the other channels when configured."
189
+ hint="Choose which channels receive inbound chat. Outbound send can still work on any configured channel."
182
190
  >
183
- <ActiveChannelToggle
184
- original={originalChannel}
191
+ <ChannelEnablementControl
192
+ pageChannel="slack"
193
+ snapshot={state.kind === 'ready' ? state.data : {}}
194
+ enabledChannels={enabledChannels}
195
+ homeChannel={homeChannel}
196
+ setEnabledChannels={setEnabledChannels}
197
+ setHomeChannel={setHomeChannel}
185
198
  dirty={dirty}
186
199
  idPrefix="sl-channel"
187
200
  />
@@ -333,3 +346,7 @@ export default function ChannelsSlack({ port, client, dirty, registerSave }: Set
333
346
  </form>
334
347
  );
335
348
  }
349
+
350
+ function isMessengerChannel(value: unknown): value is MessengerChannel {
351
+ return value === 'telegram' || value === 'discord' || value === 'slack';
352
+ }
@@ -1,7 +1,8 @@
1
1
  // Phase 3 — Channels (Telegram) page.
2
2
  //
3
3
  // Settings keys:
4
- // channel (shared across telegram + discord pages)
4
+ // messaging.enabledChannels
5
+ // messaging.homeChannel
5
6
  // telegram.enabled
6
7
  // telegram.token (SecretField, masked, never logged)
7
8
  // telegram.allowedChatIds (number[], rendered as numeric chips)
@@ -19,10 +20,15 @@ import {
19
20
  usePageSnapshot,
20
21
  } from './page-shell';
21
22
  import { expandPatch } from './path-utils';
22
- import { ActiveChannelToggle } from './components/ActiveChannelToggle';
23
- import type { ActiveChannel } from './components/ActiveChannelToggle';
24
23
  import { HealthBadge, interpretTelegramProbe } from './components/HealthBadge';
24
+ import type { MessengerChannel } from './components/ChannelEnablementControl';
25
25
  import { TransportStatusChips } from './components/TransportStatusChips';
26
+ import { ChannelEnablementControl } from './components/ChannelEnablementControl';
27
+
28
+ type MessagingBlock = {
29
+ enabledChannels?: MessengerChannel[];
30
+ homeChannel?: MessengerChannel;
31
+ };
26
32
 
27
33
  type TelegramBlock = {
28
34
  enabled?: boolean;
@@ -33,13 +39,15 @@ type TelegramBlock = {
33
39
  };
34
40
 
35
41
  type TelegramSnapshot = {
36
- channel?: ActiveChannel;
42
+ channel?: MessengerChannel;
37
43
  telegram?: TelegramBlock;
44
+ messaging?: MessagingBlock;
38
45
  [key: string]: unknown;
39
46
  };
40
47
 
41
48
  const TELEGRAM_KEYS = [
42
- 'channel',
49
+ 'messaging.enabledChannels',
50
+ 'messaging.homeChannel',
43
51
  'telegram.enabled',
44
52
  'telegram.token',
45
53
  'telegram.allowedChatIds',
@@ -91,6 +99,8 @@ export function chipsToChatIds(chips: ReadonlyArray<string>): number[] {
91
99
  export default function ChannelsTelegram({ port, client, dirty, registerSave }: SettingsPageProps) {
92
100
  const { state, refresh, setData } = usePageSnapshot<TelegramSnapshot>(client, '/api/settings');
93
101
 
102
+ const [enabledChannels, setEnabledChannels] = useState<MessengerChannel[]>([]);
103
+ const [homeChannel, setHomeChannel] = useState<MessengerChannel>('telegram');
94
104
  const [enabled, setEnabled] = useState(false);
95
105
  const [token, setToken] = useState('');
96
106
  const [chips, setChips] = useState<string[]>([]);
@@ -99,6 +109,10 @@ export default function ChannelsTelegram({ port, client, dirty, registerSave }:
99
109
 
100
110
  useEffect(() => {
101
111
  if (state.kind !== 'ready') return;
112
+ const messaging = state.data.messaging || {};
113
+ const rawEnabled = Array.isArray(messaging.enabledChannels) ? messaging.enabledChannels : [];
114
+ setEnabledChannels(rawEnabled.filter(isMessengerChannel) as MessengerChannel[]);
115
+ setHomeChannel(isMessengerChannel(messaging.homeChannel) ? messaging.homeChannel : 'telegram');
102
116
  const tg = state.data.telegram || {};
103
117
  setEnabled(Boolean(tg.enabled));
104
118
  // Token field starts empty — we never seed the actual secret into the
@@ -126,8 +140,6 @@ export default function ChannelsTelegram({ port, client, dirty, registerSave }:
126
140
  return state.data.telegram || {};
127
141
  }, [state]);
128
142
 
129
- const originalChannel = state.kind === 'ready' ? state.data.channel : undefined;
130
-
131
143
  const onSave = useCallback(async () => {
132
144
  const bundle = dirty.saveBundle();
133
145
  if (Object.keys(bundle).length === 0) return;
@@ -138,6 +150,13 @@ export default function ChannelsTelegram({ port, client, dirty, registerSave }:
138
150
  : updated) as TelegramSnapshot;
139
151
  dirty.clear();
140
152
  setData(fresh);
153
+ const freshMessaging = fresh.messaging || {};
154
+ setEnabledChannels(
155
+ Array.isArray(freshMessaging.enabledChannels)
156
+ ? freshMessaging.enabledChannels.filter(isMessengerChannel) as MessengerChannel[]
157
+ : [],
158
+ );
159
+ setHomeChannel(isMessengerChannel(freshMessaging.homeChannel) ? freshMessaging.homeChannel : 'telegram');
141
160
  const tg = fresh.telegram || {};
142
161
  setEnabled(Boolean(tg.enabled));
143
162
  setToken('');
@@ -174,10 +193,15 @@ export default function ChannelsTelegram({ port, client, dirty, registerSave }:
174
193
  >
175
194
  <SettingsSection
176
195
  title="Channels"
177
- hint="Choose which channel receives inbound chat. Outbound send can still work on the other channel when configured."
196
+ hint="Choose which channels receive inbound chat. Outbound send can still work on any configured channel."
178
197
  >
179
- <ActiveChannelToggle
180
- original={originalChannel}
198
+ <ChannelEnablementControl
199
+ pageChannel="telegram"
200
+ snapshot={state.kind === 'ready' ? state.data : {}}
201
+ enabledChannels={enabledChannels}
202
+ homeChannel={homeChannel}
203
+ setEnabledChannels={setEnabledChannels}
204
+ setHomeChannel={setHomeChannel}
181
205
  dirty={dirty}
182
206
  idPrefix="tg-channel"
183
207
  />
@@ -277,3 +301,7 @@ export default function ChannelsTelegram({ port, client, dirty, registerSave }:
277
301
  </form>
278
302
  );
279
303
  }
304
+
305
+ function isMessengerChannel(value: unknown): value is MessengerChannel {
306
+ return value === 'telegram' || value === 'discord' || value === 'slack';
307
+ }
@@ -0,0 +1,98 @@
1
+ import { useCallback, useState } from 'react';
2
+ import type { DirtyStore } from '../../types';
3
+ import { ToggleField } from '../../fields';
4
+
5
+ export type MessengerChannel = 'telegram' | 'discord' | 'slack';
6
+
7
+ type MessagingBlock = {
8
+ enabledChannels?: MessengerChannel[];
9
+ homeChannel?: MessengerChannel;
10
+ };
11
+
12
+ type Props = {
13
+ pageChannel: MessengerChannel;
14
+ snapshot: { messaging?: MessagingBlock };
15
+ enabledChannels: MessengerChannel[];
16
+ homeChannel: MessengerChannel;
17
+ setEnabledChannels: (next: MessengerChannel[]) => void;
18
+ setHomeChannel: (next: MessengerChannel) => void;
19
+ dirty: DirtyStore;
20
+ idPrefix?: string;
21
+ };
22
+
23
+ export function ChannelEnablementControl({
24
+ pageChannel,
25
+ snapshot,
26
+ enabledChannels,
27
+ homeChannel,
28
+ setEnabledChannels,
29
+ setHomeChannel,
30
+ dirty,
31
+ idPrefix = 'channel',
32
+ }: Props) {
33
+ const messaging = snapshot.messaging || {};
34
+ const originalEnabled = Array.isArray(messaging.enabledChannels) ? messaging.enabledChannels : [];
35
+ const originalHome = messaging.homeChannel || 'telegram';
36
+
37
+ const setDirtyEnabled = useCallback((next: MessengerChannel[]) => {
38
+ dirty.set('messaging.enabledChannels', {
39
+ value: next,
40
+ original: originalEnabled,
41
+ valid: true,
42
+ });
43
+ }, [dirty, originalEnabled]);
44
+
45
+ const setDirtyHome = useCallback((next: MessengerChannel) => {
46
+ dirty.set('messaging.homeChannel', {
47
+ value: next,
48
+ original: originalHome,
49
+ valid: true,
50
+ });
51
+ }, [dirty, originalHome]);
52
+
53
+ const isEnabled = enabledChannels.includes(pageChannel);
54
+
55
+ return (
56
+ <>
57
+ <ToggleField
58
+ id={`${idPrefix}-enabled`}
59
+ label={`Receive inbound messages on ${capitalize(pageChannel)}`}
60
+ value={isEnabled}
61
+ onChange={(next) => {
62
+ const nextSet = next
63
+ ? [...new Set([...enabledChannels, pageChannel])]
64
+ : enabledChannels.filter(c => c !== pageChannel);
65
+ setEnabledChannels(nextSet);
66
+ setDirtyEnabled(nextSet);
67
+ if (next && enabledChannels.length === 0) {
68
+ setHomeChannel(pageChannel);
69
+ setDirtyHome(pageChannel);
70
+ }
71
+ }}
72
+ />
73
+ <fieldset className="settings-field">
74
+ <legend className="settings-field-label">Home channel (fallback for proactive sends)</legend>
75
+ <div className="settings-active-channel-options" role="radiogroup" aria-label="Home channel">
76
+ {(['telegram', 'discord', 'slack'] as const).map((ch) => (
77
+ <label key={ch} className="settings-radio">
78
+ <input
79
+ type="radio"
80
+ name={`${idPrefix}-homeChannel`}
81
+ checked={homeChannel === ch}
82
+ onChange={() => {
83
+ setHomeChannel(ch);
84
+ setDirtyHome(ch);
85
+ }}
86
+ />
87
+ <span>{capitalize(ch)}</span>
88
+ </label>
89
+ ))}
90
+ </div>
91
+ </fieldset>
92
+ </>
93
+ );
94
+ }
95
+
96
+ function capitalize(value: string): string {
97
+ return value.charAt(0).toUpperCase() + value.slice(1);
98
+ }