bothouse-dsh 0.0.0-stage → 0.1.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.
@@ -0,0 +1,2676 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "bothouse-dsh",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ (function(){var s=document.createElement('style');s.setAttribute('data-dsh-plugin',"bothouse-dsh");s.textContent=".OEONaq_page {\n width: 100%;\n height: 100%;\n min-height: 0;\n padding-top: var(--dsh-frame-top-clearance, 0px);\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-base);\n grid-template-columns: 220px minmax(0, 1fr) 240px;\n font-size: 14px;\n line-height: 1.5;\n display: grid;\n}\n\n.OEONaq_notice {\n max-width: 480px;\n color: var(--dsw-alias-label-secondary);\n text-align: center;\n grid-column: 1 / -1;\n place-self: center;\n padding: 24px;\n}\n\n.OEONaq_rooms, .OEONaq_members {\n background: var(--dsw-alias-bg-layer-2);\n min-height: 0;\n padding: 8px 8px 16px;\n overflow-y: auto;\n}\n\n.OEONaq_groupTitle {\n color: var(--dsw-alias-label-tertiary);\n letter-spacing: .02em;\n margin: 12px 8px 6px;\n font-size: 12px;\n font-weight: 600;\n}\n\n.OEONaq_room {\n border-radius: var(--dsw-radius-sm);\n width: 100%;\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n text-align: left;\n cursor: pointer;\n background: none;\n border: 0;\n align-items: center;\n gap: 6px;\n padding: 6px 8px;\n display: flex;\n}\n\n.OEONaq_room:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.OEONaq_room:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color);\n}\n\n.OEONaq_workspace {\n padding-left: 22px;\n font-size: .95em;\n}\n\n.OEONaq_roomLabel {\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.OEONaq_activeCount {\n color: var(--dsw-alias-label-tertiary);\n flex-shrink: 0;\n margin-left: auto;\n font-size: 11px;\n}\n\n.OEONaq_activeBusy {\n color: var(--dsw-alias-label-primary);\n}\n\n.OEONaq_roomActive {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.OEONaq_hash {\n color: var(--dsw-alias-label-tertiary);\n font-size: 16px;\n}\n\n.OEONaq_chat {\n flex-direction: column;\n min-width: 0;\n min-height: 0;\n display: flex;\n}\n\n.OEONaq_chatHead {\n border-bottom: 1px solid var(--dsw-alias-border-l2);\n align-items: center;\n gap: 6px;\n height: 44px;\n padding: 0 16px;\n font-weight: 600;\n display: flex;\n}\n\n.OEONaq_log {\n flex: 1;\n min-height: 0;\n padding: 8px 0 12px;\n overflow-y: auto;\n}\n\n.OEONaq_guide {\n border: 1px dashed var(--dsw-alias-border-l2);\n border-radius: var(--dsw-radius-sm);\n color: var(--dsw-alias-label-secondary);\n flex-direction: column;\n align-items: flex-start;\n gap: 10px;\n margin: 16px;\n padding: 14px 16px;\n display: flex;\n}\n\n.OEONaq_empty {\n color: var(--dsw-alias-label-tertiary);\n padding: 24px 16px;\n}\n\n.OEONaq_message {\n grid-template-columns: 36px minmax(0, 1fr);\n gap: 12px;\n padding: 6px 16px;\n display: grid;\n}\n\n.OEONaq_message:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.OEONaq_system .OEONaq_text {\n color: var(--dsw-alias-label-tertiary);\n font-style: italic;\n}\n\n.OEONaq_avatar {\n corner-shape: round;\n color: #fff;\n border-radius: 50%;\n place-items: center;\n width: 36px;\n height: 36px;\n font-weight: 700;\n display: grid;\n position: relative;\n}\n\n.OEONaq_avatarImage {\n object-fit: cover;\n border-radius: 50%;\n width: 100%;\n height: 100%;\n position: absolute;\n inset: 0;\n}\n\n.OEONaq_avatarSmall {\n width: 28px;\n height: 28px;\n font-size: 12px;\n}\n\n.OEONaq_avatarAnchor {\n align-self: start;\n position: relative;\n}\n\n.OEONaq_avatarButton {\n cursor: pointer;\n background: none;\n border: 0;\n border-radius: 50%;\n padding: 0;\n}\n\n.OEONaq_avatarMenu {\n z-index: 3;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-alias-bg-base);\n min-width: 140px;\n box-shadow: var(--dsw-elevation-panel, 0 8px 24px #0000003d);\n flex-direction: column;\n padding: 6px;\n display: flex;\n position: absolute;\n top: calc(100% + 4px);\n left: 0;\n}\n\n.OEONaq_avatarMenuTitle {\n padding: 4px 8px;\n font-weight: 700;\n}\n\n.OEONaq_avatarMenuItem {\n border-radius: var(--dsw-radius-sm);\n color: inherit;\n text-align: left;\n cursor: pointer;\n background: none;\n border: 0;\n padding: 6px 8px;\n}\n\n.OEONaq_avatarMenuItem:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.OEONaq_messageBody {\n min-width: 0;\n}\n\n.OEONaq_messageHead {\n align-items: baseline;\n gap: 6px;\n display: flex;\n}\n\n.OEONaq_author {\n font-weight: 600;\n}\n\n.OEONaq_time {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n}\n\n.OEONaq_tag {\n border-radius: var(--dsw-radius-xs);\n color: var(--dsw-alias-label-primary-inverted);\n background: var(--dsw-alias-state-business-primary);\n white-space: nowrap;\n padding: 0 4px;\n font-size: 10px;\n font-weight: 600;\n}\n\n.OEONaq_text {\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n\n.OEONaq_card {\n border-left: 4px solid var(--dsw-alias-state-business-primary);\n border-radius: var(--dsw-radius-sm);\n background: var(--dsw-alias-bg-layer-2);\n margin-top: 4px;\n padding: 6px 10px;\n}\n\n.OEONaq_card[data-state=\"failed\"] {\n border-left-color: var(--dsw-alias-state-error-primary);\n}\n\n.OEONaq_cardTitle {\n font-size: 13px;\n font-weight: 600;\n}\n\n.OEONaq_cardHead {\n justify-content: space-between;\n align-items: center;\n gap: 8px;\n display: flex;\n}\n\n.OEONaq_cardViews {\n gap: 4px;\n display: flex;\n}\n\n.OEONaq_cardViewButton {\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: var(--dsw-radius-sm);\n color: inherit;\n cursor: pointer;\n background: none;\n padding: 0 6px;\n font-size: 12px;\n}\n\n.OEONaq_cardViewButton[aria-pressed=\"true\"] {\n background: var(--dsw-alias-bg-base);\n font-weight: 600;\n}\n\n.OEONaq_permActions {\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n margin-top: 6px;\n display: flex;\n}\n\n.OEONaq_permScope {\n color: var(--dsw-alias-text-secondary);\n flex-basis: 100%;\n font-size: 12px;\n}\n\n.OEONaq_permStatus {\n margin-top: 6px;\n font-size: 13px;\n font-weight: 600;\n}\n\n.OEONaq_message {\n position: relative;\n}\n\n.OEONaq_quote {\n border-left: 2px solid var(--dsw-alias-label-tertiary);\n min-width: 0;\n color: var(--dsw-alias-label-tertiary);\n white-space: nowrap;\n text-overflow: ellipsis;\n gap: 6px;\n margin-bottom: 2px;\n padding-left: 8px;\n font-size: 12px;\n display: flex;\n overflow: hidden;\n}\n\n.OEONaq_quoteDeleted {\n font-style: italic;\n}\n\n.OEONaq_quoteAuthor {\n flex: none;\n font-weight: 600;\n}\n\n.OEONaq_replyButton {\n opacity: 0;\n border-radius: var(--dsw-radius-xs);\n color: var(--dsw-alias-label-tertiary);\n background: var(--dsw-alias-bg-layer-2);\n cursor: pointer;\n border: 0;\n padding: 0 6px;\n position: absolute;\n top: 4px;\n right: 16px;\n}\n\n.OEONaq_message:hover .OEONaq_replyButton, .OEONaq_message:focus-within .OEONaq_replyButton, .OEONaq_replyButton:focus-visible {\n opacity: 1;\n}\n\n@media (hover: none) {\n .OEONaq_replyButton {\n opacity: 1;\n }\n}\n\n.OEONaq_replyBar {\n border-radius: var(--dsw-radius-sm);\n color: var(--dsw-alias-label-tertiary);\n background: var(--dsw-alias-bg-layer-2);\n align-items: center;\n gap: 8px;\n margin-bottom: 6px;\n padding: 4px 8px;\n font-size: 12px;\n display: flex;\n}\n\n.OEONaq_replyBarText {\n white-space: nowrap;\n text-overflow: ellipsis;\n flex: 1;\n min-width: 0;\n overflow: hidden;\n}\n\n.OEONaq_replyCancel {\n color: inherit;\n cursor: pointer;\n background: none;\n border: 0;\n flex: none;\n}\n\n.OEONaq_working {\n min-height: 20px;\n color: var(--dsw-alias-label-tertiary);\n padding: 0 16px;\n font-size: 12px;\n}\n\n.OEONaq_composer {\n padding: 0 16px 16px;\n position: relative;\n}\n\n.OEONaq_composerRow {\n align-items: flex-end;\n gap: 8px;\n display: flex;\n}\n\n.OEONaq_mentionButton {\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: var(--dsw-radius-md);\n width: 40px;\n height: 40px;\n color: var(--dsw-alias-label-secondary);\n background: var(--dsw-alias-bg-layer-2);\n font: inherit;\n cursor: pointer;\n flex: none;\n font-size: 18px;\n font-weight: 600;\n}\n\n.OEONaq_mentionButton:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.OEONaq_mentionButton:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color);\n}\n\n.OEONaq_mentionPopup {\n z-index: 2;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-alias-bg-base);\n max-height: 320px;\n box-shadow: var(--dsw-elevation-panel, 0 8px 24px #0000003d);\n flex-direction: column;\n padding: 6px;\n display: flex;\n position: absolute;\n bottom: calc(100% - 8px);\n left: 16px;\n right: 16px;\n}\n\n.OEONaq_mentionPopup > [role=\"listbox\"] {\n min-height: 0;\n overflow-y: auto;\n}\n\n.OEONaq_mentionTitle {\n color: var(--dsw-alias-label-tertiary);\n padding: 4px 8px 6px;\n font-size: 12px;\n font-weight: 600;\n}\n\n.OEONaq_mentionItem {\n border-radius: var(--dsw-radius-sm);\n cursor: pointer;\n align-items: center;\n gap: 10px;\n padding: 6px 8px;\n display: flex;\n}\n\n.OEONaq_mentionActive {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.OEONaq_mentionName {\n font-weight: 600;\n}\n\n.OEONaq_mentionMeta {\n color: var(--dsw-alias-label-tertiary);\n margin-left: auto;\n font-size: 12px;\n}\n\n.OEONaq_mentionHint {\n border-top: 1px solid var(--dsw-alias-border-l2);\n color: var(--dsw-alias-label-tertiary);\n padding: 6px 8px 2px;\n font-size: 11px;\n}\n\n.OEONaq_sendError {\n color: var(--dsw-alias-state-error-primary);\n margin-bottom: 6px;\n font-size: 12px;\n}\n\n.OEONaq_input {\n box-sizing: border-box;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: var(--dsw-radius-md);\n width: 100%;\n max-height: 40vh;\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-layer-2);\n font: inherit;\n resize: none;\n field-sizing: content;\n padding: 10px 14px;\n}\n\n.OEONaq_input:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color);\n}\n\n.OEONaq_member {\n border-radius: var(--dsw-radius-sm);\n min-width: 0;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n background: none;\n border: 0;\n flex: 1;\n align-items: center;\n gap: 10px;\n padding: 5px 8px;\n display: flex;\n}\n\n.OEONaq_member:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.OEONaq_member:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color);\n}\n\n.OEONaq_offline {\n opacity: .5;\n}\n\n.OEONaq_memberAvatar {\n position: relative;\n}\n\n.OEONaq_presence {\n border: 2px solid var(--dsw-alias-bg-layer-2);\n corner-shape: round;\n background: #23a55a;\n border-radius: 50%;\n width: 10px;\n height: 10px;\n position: absolute;\n bottom: -2px;\n right: -2px;\n}\n\n.OEONaq_offline .OEONaq_presence {\n background: var(--dsw-alias-label-tertiary);\n}\n\n.OEONaq_memberText {\n flex-direction: column;\n min-width: 0;\n display: flex;\n}\n\n.OEONaq_memberName {\n align-items: center;\n gap: 6px;\n display: flex;\n}\n\n.OEONaq_memberStatus {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n}\n\n@media (width <= 900px) {\n .OEONaq_page {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .OEONaq_rooms, .OEONaq_members {\n display: none;\n }\n}\n\n.OEONaq_groupHead {\n justify-content: space-between;\n align-items: center;\n padding-right: 4px;\n display: flex;\n}\n\n.OEONaq_iconButton {\n border-radius: var(--dsw-radius-sm);\n width: 22px;\n height: 22px;\n color: var(--dsw-alias-label-tertiary);\n font: inherit;\n cursor: pointer;\n background: none;\n border: 0;\n font-size: 16px;\n line-height: 1;\n}\n\n.OEONaq_iconButton:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.OEONaq_roomInput, .OEONaq_fieldInput {\n box-sizing: border-box;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: var(--dsw-radius-sm);\n width: 100%;\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-base);\n font: inherit;\n padding: 6px 8px;\n}\n\n.OEONaq_roomInput {\n margin: 2px 0 6px;\n}\n\n.OEONaq_roomInput:focus-visible, .OEONaq_fieldInput:focus-visible, .OEONaq_iconButton:focus-visible, .OEONaq_linkButton:focus-visible, .OEONaq_addMemberButton:focus-visible, .OEONaq_primaryButton:focus-visible, .OEONaq_secondaryButton:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color);\n}\n\n.OEONaq_addMemberButton {\n border: 1px dashed var(--dsw-alias-border-l2);\n border-radius: var(--dsw-radius-sm);\n width: 100%;\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n cursor: pointer;\n background: none;\n margin: 4px 0;\n padding: 7px 8px;\n}\n\n.OEONaq_addMemberButton:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.OEONaq_memberRow {\n border-radius: var(--dsw-radius-sm);\n align-items: center;\n display: flex;\n position: relative;\n}\n\n.OEONaq_memberRow:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.OEONaq_memberRow .OEONaq_member:hover {\n background: none;\n}\n\n.OEONaq_memberModel {\n color: var(--dsw-alias-label-tertiary);\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 11px;\n overflow: hidden;\n}\n\n.OEONaq_memberActions {\n color: var(--dsw-alias-label-tertiary);\n opacity: 0;\n flex: none;\n gap: 4px;\n padding-right: 6px;\n font-size: 12px;\n display: flex;\n}\n\n.OEONaq_memberRow:hover .OEONaq_memberActions, .OEONaq_memberRow:focus-within .OEONaq_memberActions {\n opacity: 1;\n}\n\n.OEONaq_linkButton {\n border-radius: var(--dsw-radius-xs);\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n cursor: pointer;\n background: none;\n border: 0;\n padding: 2px 4px;\n font-size: 12px;\n}\n\n.OEONaq_linkButton:hover {\n color: var(--dsw-alias-label-primary);\n text-decoration: underline;\n}\n\n.OEONaq_dialogBackdrop {\n z-index: 10;\n background: #00000073;\n place-items: center;\n padding: 16px;\n display: grid;\n position: fixed;\n inset: 0;\n}\n\n.OEONaq_dialog {\n border-radius: var(--dsw-radius-lg);\n width: min(460px, 100%);\n max-height: calc(100vh - 32px);\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-base);\n box-shadow: var(--dsw-elevation-panel, 0 12px 32px #0000004d);\n flex-direction: column;\n gap: 12px;\n padding: 20px;\n display: flex;\n overflow-y: auto;\n}\n\n.OEONaq_dialogTitle {\n margin: 0;\n font-size: 16px;\n}\n\n.OEONaq_field {\n color: var(--dsw-alias-label-secondary);\n flex-direction: column;\n gap: 4px;\n font-size: 12px;\n display: flex;\n}\n\n.OEONaq_field textarea {\n resize: vertical;\n}\n\n.OEONaq_choiceRow {\n flex-wrap: wrap;\n gap: 12px;\n display: flex;\n}\n\n.OEONaq_choice {\n color: var(--dsw-alias-label-primary, inherit);\n cursor: pointer;\n align-items: center;\n gap: 4px;\n font-size: 13px;\n display: inline-flex;\n}\n\n.OEONaq_dialogActions {\n justify-content: flex-end;\n gap: 8px;\n display: flex;\n}\n\n.OEONaq_primaryButton, .OEONaq_secondaryButton {\n border-radius: var(--dsw-radius-sm);\n font: inherit;\n cursor: pointer;\n padding: 7px 14px;\n}\n\n.OEONaq_primaryButton {\n color: #fff;\n background: var(--dsw-alias-state-business-primary);\n border: 0;\n}\n\n.OEONaq_primaryButton:disabled {\n cursor: not-allowed;\n opacity: .5;\n}\n\n.OEONaq_secondaryButton {\n border: 1px solid var(--dsw-alias-border-l2);\n color: var(--dsw-alias-label-primary);\n background: none;\n}\n\n.OEONaq_memberFailed .OEONaq_memberStatus {\n color: var(--dsw-alias-state-error-primary, #ed4245);\n}\n\n.OEONaq_memberFailed .OEONaq_presence {\n background: #ed4245;\n}\n\n.OEONaq_mobileButton, .OEONaq_drawerBackdrop {\n display: none;\n}\n\n.OEONaq_panelToggle {\n border-radius: var(--dsw-radius-sm);\n width: 28px;\n height: 28px;\n color: var(--dsw-alias-label-tertiary);\n font: inherit;\n cursor: pointer;\n background: none;\n border: 0;\n justify-content: center;\n align-items: center;\n font-size: 16px;\n line-height: 1;\n display: inline-flex;\n}\n\n.OEONaq_panelToggle:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.OEONaq_panelToggle:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color);\n}\n\n.OEONaq_panelToggleEnd {\n margin-left: auto;\n}\n\n.OEONaq_capabilityMenu {\n z-index: 4;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: var(--dsw-radius-sm);\n background: var(--dsw-alias-bg-base);\n max-height: 260px;\n box-shadow: var(--dsw-elevation-panel, 0 8px 24px #0000003d);\n gap: 2px;\n padding: 5px;\n display: grid;\n position: absolute;\n bottom: calc(100% + 4px);\n left: 0;\n right: 0;\n overflow-y: auto;\n}\n\n.OEONaq_capabilityItem:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.OEONaq_capabilityHelp {\n color: var(--dsw-alias-label-secondary);\n white-space: normal;\n margin: 6px;\n font-size: 12px;\n}\n\n.OEONaq_capabilityItem:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color);\n}\n\n.OEONaq_capabilityItem {\n text-align: left;\n cursor: pointer;\n color: var(--dsw-alias-label-secondary);\n white-space: nowrap;\n background: none;\n border: 0;\n padding: 5px 8px;\n font-size: 12px;\n font-weight: 400;\n}\n\n@media (width >= 901px) {\n .OEONaq_page.OEONaq_roomsHidden {\n grid-template-columns: minmax(0, 1fr) 240px;\n }\n\n .OEONaq_page.OEONaq_membersHidden {\n grid-template-columns: 220px minmax(0, 1fr);\n }\n\n .OEONaq_page.OEONaq_roomsHidden.OEONaq_membersHidden {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .OEONaq_roomsHidden > .OEONaq_rooms, .OEONaq_membersHidden > .OEONaq_members {\n display: none;\n }\n}\n\n@media (width <= 900px) {\n .OEONaq_page {\n position: relative;\n overflow: hidden;\n }\n\n .OEONaq_mobileButton {\n min-width: 40px;\n min-height: 40px;\n color: inherit;\n font: inherit;\n cursor: pointer;\n background: none;\n border: 0;\n border-radius: 6px;\n justify-content: center;\n align-items: center;\n display: inline-flex;\n }\n\n .OEONaq_mobileButton:last-child {\n margin-left: auto;\n }\n\n .OEONaq_panelToggle {\n display: none;\n }\n\n .OEONaq_drawerBackdrop {\n z-index: 4;\n background: #00000073;\n border: 0;\n display: block;\n position: absolute;\n inset: 0;\n }\n\n .OEONaq_rooms.OEONaq_drawerOpen, .OEONaq_members.OEONaq_drawerOpen {\n z-index: 5;\n box-sizing: border-box;\n width: min(280px, 85%);\n display: block;\n position: absolute;\n top: 0;\n bottom: 0;\n box-shadow: 0 0 24px #0000004d;\n }\n\n .OEONaq_rooms.OEONaq_drawerOpen {\n left: 0;\n }\n\n .OEONaq_members.OEONaq_drawerOpen {\n right: 0;\n }\n\n .OEONaq_memberActions {\n opacity: 1;\n }\n\n .OEONaq_messageHead {\n flex-wrap: wrap;\n }\n}\n\n.OEONaq_avatarPicker {\n flex-wrap: wrap;\n gap: 8px;\n display: flex;\n}\n\n.OEONaq_avatarOption {\n cursor: pointer;\n background: none;\n border: 2px solid #0000;\n border-radius: 50%;\n padding: 2px;\n display: grid;\n}\n\n.OEONaq_avatarOption[aria-checked=\"true\"] {\n border-color: var(--dsw-alias-accent, #5865f2);\n}\n\n.OEONaq_taskBoard {\n border-bottom: 1px solid var(--dsw-alias-border-l2);\n margin-bottom: 8px;\n padding-bottom: 8px;\n}\n\n.OEONaq_leadControl {\n border-bottom: 1px solid var(--dsw-alias-border-l2);\n margin: 0 0 10px;\n padding: 0 8px 10px;\n}\n\n.OEONaq_leadControl .OEONaq_field {\n color: var(--dsw-alias-label-tertiary);\n gap: 5px;\n font-size: 12px;\n}\n\n.OEONaq_fieldLabel {\n color: var(--dsw-alias-label-tertiary);\n margin-bottom: 5px;\n font-size: 12px;\n display: block;\n}\n\n.OEONaq_leadControl .OEONaq_fieldInput {\n width: 100%;\n color: var(--dsw-alias-label-primary);\n text-align: left;\n cursor: pointer;\n}\n\n.OEONaq_leadMenu {\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: var(--dsw-radius-sm);\n background: var(--dsw-alias-bg-base);\n gap: 2px;\n margin-top: 4px;\n padding: 4px;\n display: grid;\n}\n\n.OEONaq_leadOption {\n border-radius: var(--dsw-radius-xs);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n text-align: left;\n cursor: pointer;\n background: none;\n border: 0;\n padding: 5px 8px;\n}\n\n.OEONaq_leadOption:hover, .OEONaq_leadOption[aria-selected=\"true\"] {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.OEONaq_taskSection + .OEONaq_taskSection {\n margin-top: 8px;\n}\n\n.OEONaq_taskSectionTitle {\n color: var(--dsw-alias-label-secondary);\n margin: 0 0 4px;\n padding: 0 8px;\n font-size: 12px;\n font-weight: 600;\n}\n\n.OEONaq_taskOwnerGroup + .OEONaq_taskOwnerGroup {\n margin-top: 4px;\n}\n\n.OEONaq_taskOwnerName {\n color: var(--dsw-alias-label-tertiary);\n padding: 0 8px;\n font-size: 12px;\n}\n\n.OEONaq_taskList {\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.OEONaq_taskRow {\n border-radius: var(--dsw-radius-sm);\n padding: 6px 8px;\n}\n\n.OEONaq_taskRow:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.OEONaq_taskLine {\n align-items: center;\n gap: 6px;\n min-width: 0;\n display: flex;\n}\n\n.OEONaq_taskTitle {\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 13px;\n overflow: hidden;\n}\n\n.OEONaq_taskMeta {\n color: var(--dsw-alias-label-tertiary);\n flex-wrap: wrap;\n gap: 0 8px;\n font-size: 12px;\n display: flex;\n}\n\n.OEONaq_taskLinks {\n gap: 8px;\n display: flex;\n}\n\n.OEONaq_taskEmpty {\n color: var(--dsw-alias-label-tertiary);\n margin: 0 8px;\n font-size: 12px;\n}\n\n.OEONaq_taskDone summary {\n cursor: pointer;\n}\n\n.OEONaq_taskStatus {\n border-radius: var(--dsw-radius-xs);\n color: var(--dsw-alias-label-primary-inverted);\n background: var(--dsw-alias-state-business-primary);\n flex: none;\n padding: 0 4px;\n font-size: 10px;\n font-style: normal;\n font-weight: 600;\n}\n\n.OEONaq_taskStatus[data-status=\"asked\"], .OEONaq_taskStatus[data-status=\"escalated\"], .OEONaq_taskStatus[data-status=\"declined\"], .OEONaq_taskStatus[data-status=\"rework\"] {\n background: var(--dsw-alias-state-warning-primary, var(--dsw-alias-state-error-primary));\n}\n\n.OEONaq_taskStatus[data-status=\"done\"] {\n background: var(--dsw-alias-label-tertiary);\n}\n\n.OEONaq_taskCard {\n margin-top: 4px;\n font-style: normal;\n}\n\n.OEONaq_taskMembers {\n margin: 4px 0 0;\n padding: 0;\n list-style: none;\n}\n\n.OEONaq_taskMember {\n align-items: center;\n gap: 8px;\n font-size: 13px;\n display: flex;\n}\n\n.OEONaq_taskMemberName {\n font-weight: 600;\n}\n\n.OEONaq_taskSub {\n color: var(--dsw-alias-label-secondary);\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.OEONaq_focused {\n background: var(--dsw-alias-interactive-bg-hover);\n outline: 2px solid var(--dsw-alias-state-business-primary);\n}\n";document.head.appendChild(s)})();
7
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
8
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
+ let react = require("react");
11
+ // src/client/channel-source.ts
12
+ /**
13
+ * Order rooms as chat channel, then its workspaces, in id order. A workspace whose
14
+ * chat channel is missing is shown at the top level so it stays reachable.
15
+ */
16
+ function groupRooms(rooms) {
17
+ const chats = new Set(rooms.filter((r) => r.kind !== "workspace").map((r) => r.id));
18
+ const under = /* @__PURE__ */ new Map();
19
+ for (const r of rooms) if (r.kind === "workspace" && r.parent_id != null && chats.has(r.parent_id)) under.set(r.parent_id, [...under.get(r.parent_id) ?? [], r]);
20
+ const rows = [];
21
+ for (const r of rooms) if (r.kind !== "workspace") {
22
+ rows.push({
23
+ room: r,
24
+ depth: 0
25
+ });
26
+ for (const w of under.get(r.id) ?? []) rows.push({
27
+ room: w,
28
+ depth: 1
29
+ });
30
+ } else if (r.parent_id == null || !chats.has(r.parent_id)) rows.push({
31
+ room: r,
32
+ depth: 0
33
+ });
34
+ return rows;
35
+ }
36
+ const INITIAL = {
37
+ phase: "idle",
38
+ error: void 0,
39
+ rooms: [],
40
+ roomId: void 0,
41
+ members: [],
42
+ thinking: {},
43
+ messages: [],
44
+ leadId: null,
45
+ tasks: [],
46
+ taskChat: void 0
47
+ };
48
+ /** Status-coded service failure so the panel can tell "not enabled" from "not signed in". */
49
+ var ServiceError = class extends Error {
50
+ status;
51
+ constructor(status, message) {
52
+ super(message);
53
+ this.status = status;
54
+ this.name = "ServiceError";
55
+ }
56
+ };
57
+ function json(method, body) {
58
+ return {
59
+ method,
60
+ headers: { "content-type": "application/json" },
61
+ body: JSON.stringify(body)
62
+ };
63
+ }
64
+ /**
65
+ * Browser transport over the page's own origin and document base.
66
+ * @returns fetch and EventSource bound to the current document.
67
+ */
68
+ function browserTransport() {
69
+ return {
70
+ fetch: (input, init) => fetch(input, {
71
+ credentials: "same-origin",
72
+ ...init
73
+ }),
74
+ openEvents: (url) => new EventSource(url, { withCredentials: true }),
75
+ url: (path) => new URL(path, document.baseURI).toString()
76
+ };
77
+ }
78
+ /**
79
+ * Create the channel source and its actions.
80
+ * @param transport - fetch/EventSource binding.
81
+ * @returns the observable snapshot, the actions, and a disposer.
82
+ */
83
+ function createChannelSource(transport) {
84
+ let snapshot = INITIAL;
85
+ const listeners = /* @__PURE__ */ new Set();
86
+ let stream;
87
+ let lifetime;
88
+ let pending;
89
+ const set = (next) => {
90
+ snapshot = {
91
+ ...snapshot,
92
+ ...next
93
+ };
94
+ for (const listener of listeners) listener();
95
+ };
96
+ const request = async (path, init) => {
97
+ const response = await transport.fetch(transport.url(path), init);
98
+ if (!response.ok) {
99
+ const body = await response.json().catch(() => ({}));
100
+ throw new ServiceError(response.status, body.error ?? response.statusText);
101
+ }
102
+ return await response.json();
103
+ };
104
+ const fail = (error) => {
105
+ if (error instanceof ServiceError && error.status === 404) set({
106
+ phase: "unavailable",
107
+ error: void 0
108
+ });
109
+ else if (error instanceof ServiceError && error.status === 401) set({
110
+ phase: "unauthorized",
111
+ error: void 0
112
+ });
113
+ else set({
114
+ phase: "error",
115
+ error: error instanceof Error ? error.message : String(error)
116
+ });
117
+ };
118
+ const earlyTasks = /* @__PURE__ */ new Map();
119
+ const newer = (a, b) => (a?.updated_at ?? -Infinity) <= b.updated_at;
120
+ const applyEvent = (current, event) => {
121
+ switch (event.type) {
122
+ case "message": {
123
+ if (event.message.room_id !== current.roomId) return current;
124
+ const rows = new Map(current.messages.map((m) => [m.id, m]));
125
+ rows.set(event.message.id, event.message);
126
+ return {
127
+ ...current,
128
+ messages: [...rows.values()].sort((a, b) => a.id - b.id)
129
+ };
130
+ }
131
+ case "deleted": return event.room_id === current.roomId ? {
132
+ ...current,
133
+ messages: current.messages.filter((m) => m.id !== event.id).map((m) => m.reply_to?.id === event.id ? {
134
+ ...m,
135
+ reply_to: {
136
+ id: event.id,
137
+ deleted: true
138
+ }
139
+ } : m)
140
+ } : current;
141
+ case "members": return {
142
+ ...current,
143
+ members: event.members
144
+ };
145
+ case "thinking": return {
146
+ ...current,
147
+ thinking: {
148
+ ...current.thinking,
149
+ [String(event.room_id)]: event.members
150
+ }
151
+ };
152
+ case "rooms": return {
153
+ ...current,
154
+ rooms: event.rooms
155
+ };
156
+ case "lead": return {
157
+ ...current,
158
+ leadId: event.lead_id,
159
+ effectiveLeadId: event.effective_lead_id ?? event.lead_id
160
+ };
161
+ case "task": {
162
+ if (current.taskChat === void 0) {
163
+ if (newer(earlyTasks.get(event.task.id)?.task, event.task)) earlyTasks.set(event.task.id, {
164
+ chat: event.chat_id,
165
+ task: event.task
166
+ });
167
+ return current;
168
+ }
169
+ if (event.chat_id !== current.taskChat) return current;
170
+ const rows = new Map((current.tasks ?? []).map((t) => [t.id, t]));
171
+ rows.set(event.task.id, event.task);
172
+ return {
173
+ ...current,
174
+ tasks: [...rows.values()].sort((a, b) => b.id - a.id)
175
+ };
176
+ }
177
+ default: return current;
178
+ }
179
+ };
180
+ const onEvent = (event) => {
181
+ pending?.events.push(event);
182
+ set(applyEvent(snapshot, event));
183
+ };
184
+ let boardRead;
185
+ const loadBoard = async (owner, roomId) => {
186
+ boardRead?.abort();
187
+ const read = new AbortController();
188
+ boardRead = read;
189
+ try {
190
+ const board = await request("collab/api/rooms/" + roomId + "/tasks", { signal: read.signal });
191
+ if (lifetime !== owner || boardRead !== read || snapshot.roomId !== roomId) return;
192
+ const live = snapshot.taskChat === board.chat_id ? snapshot.tasks ?? [] : [];
193
+ const early = [...earlyTasks.values()].filter((e) => e.chat === board.chat_id).map((e) => e.task);
194
+ earlyTasks.clear();
195
+ const rows = new Map(board.tasks.map((t) => [t.id, t]));
196
+ for (const t of [...live, ...early]) if (newer(rows.get(t.id), t)) rows.set(t.id, t);
197
+ set({
198
+ tasks: [...rows.values()].sort((a, b) => b.id - a.id),
199
+ taskChat: board.chat_id
200
+ });
201
+ } catch (error) {} finally {
202
+ if (boardRead === read) boardRead = void 0;
203
+ }
204
+ };
205
+ const refresh = async (resume) => {
206
+ const owner = lifetime;
207
+ if (owner === void 0) return;
208
+ pending?.controller.abort();
209
+ const read = {
210
+ controller: new AbortController(),
211
+ events: []
212
+ };
213
+ pending = read;
214
+ const previous = snapshot;
215
+ const current = () => lifetime === owner && pending === read && !read.controller.signal.aborted;
216
+ try {
217
+ const state = await request("collab/api/state", { signal: read.controller.signal });
218
+ if (!current()) return;
219
+ const roomId = state.rooms.some((r) => r.id === previous.roomId) ? previous.roomId : state.rooms[0]?.id;
220
+ const rows = [];
221
+ if (roomId !== void 0) {
222
+ let after = resume && roomId === previous.roomId ? (previous.messages[0]?.id ?? 1) - 1 : void 0;
223
+ while (current()) {
224
+ const query = after === void 0 ? "limit=100" : "after=" + after + "&limit=200";
225
+ const { messages } = await request("collab/api/rooms/" + roomId + "/messages?" + query, { signal: read.controller.signal });
226
+ if (!current()) return;
227
+ rows.push(...messages);
228
+ if (after === void 0 || messages.length < 200) break;
229
+ const next = messages.at(-1)?.id;
230
+ if (next === void 0 || next <= after) throw new Error("Channel history cursor did not advance");
231
+ after = next;
232
+ }
233
+ }
234
+ const keepBoard = roomId === previous.roomId;
235
+ if (!keepBoard) earlyTasks.clear();
236
+ let next = {
237
+ phase: "ready",
238
+ error: void 0,
239
+ rooms: state.rooms,
240
+ roomId,
241
+ members: state.members,
242
+ thinking: state.thinking,
243
+ messages: rows,
244
+ leadId: state.lead_id ?? null,
245
+ effectiveLeadId: state.effective_lead_id ?? state.lead_id ?? null,
246
+ tasks: keepBoard ? previous.tasks : [],
247
+ taskChat: keepBoard ? previous.taskChat : void 0
248
+ };
249
+ for (const event of read.events) next = applyEvent(next, event);
250
+ pending = void 0;
251
+ set(next);
252
+ if (roomId !== void 0) loadBoard(owner, roomId);
253
+ } catch (error) {
254
+ if (!current()) return;
255
+ pending = void 0;
256
+ fail(error);
257
+ }
258
+ };
259
+ const selectRoom = async (roomId) => {
260
+ if (roomId !== snapshot.roomId) earlyTasks.clear();
261
+ set(roomId === snapshot.roomId ? {
262
+ roomId,
263
+ messages: []
264
+ } : {
265
+ roomId,
266
+ messages: [],
267
+ tasks: [],
268
+ taskChat: void 0
269
+ });
270
+ await refresh(false);
271
+ };
272
+ const start = async (owner) => {
273
+ set({
274
+ phase: "loading",
275
+ error: void 0
276
+ });
277
+ await refresh(false);
278
+ if (lifetime !== owner || listeners.size === 0 || snapshot.phase !== "ready") return;
279
+ try {
280
+ const events = transport.openEvents(transport.url("collab/api/events"));
281
+ stream = events;
282
+ events.onopen = () => {
283
+ if (lifetime === owner) refresh(true);
284
+ };
285
+ events.onerror = () => {
286
+ if (lifetime !== owner) return;
287
+ pending?.controller.abort();
288
+ pending = void 0;
289
+ set({
290
+ phase: "loading",
291
+ error: void 0
292
+ });
293
+ };
294
+ events.onmessage = (event) => {
295
+ if (lifetime !== owner) return;
296
+ try {
297
+ onEvent(JSON.parse(event.data));
298
+ } catch (error) {}
299
+ };
300
+ } catch (error) {
301
+ if (lifetime === owner) fail(error);
302
+ }
303
+ };
304
+ const stop = () => {
305
+ lifetime = void 0;
306
+ pending?.controller.abort();
307
+ pending = void 0;
308
+ boardRead?.abort();
309
+ boardRead = void 0;
310
+ if (stream !== void 0) {
311
+ stream.onopen = null;
312
+ stream.onmessage = null;
313
+ stream.onerror = null;
314
+ stream.close();
315
+ stream = void 0;
316
+ }
317
+ };
318
+ return {
319
+ source: {
320
+ getSnapshot: () => snapshot,
321
+ subscribe(listener) {
322
+ listeners.add(listener);
323
+ if (lifetime === void 0) {
324
+ lifetime = {};
325
+ start(lifetime);
326
+ }
327
+ return () => {
328
+ listeners.delete(listener);
329
+ if (listeners.size === 0) stop();
330
+ };
331
+ }
332
+ },
333
+ actions: {
334
+ selectRoom,
335
+ async send(text, replyTo) {
336
+ const roomId = snapshot.roomId;
337
+ const owner = lifetime;
338
+ if (roomId === void 0 || owner === void 0) throw new Error("No channel is open");
339
+ const { message } = await request(`collab/api/rooms/${roomId}/messages`, {
340
+ method: "POST",
341
+ headers: { "content-type": "application/json" },
342
+ body: JSON.stringify(replyTo === void 0 ? {
343
+ text,
344
+ client_key: crypto.randomUUID()
345
+ } : {
346
+ text,
347
+ client_key: crypto.randomUUID(),
348
+ reply_to: replyTo
349
+ })
350
+ });
351
+ if (lifetime === owner) onEvent({
352
+ type: "message",
353
+ message
354
+ });
355
+ },
356
+ async createRoom(name) {
357
+ const { room, workspace } = await request("collab/api/rooms", json("POST", { name }));
358
+ const added = [room, workspace].filter((r) => r !== void 0 && !snapshot.rooms.some((s) => s.id === r.id));
359
+ if (added.length > 0) set({ rooms: [...snapshot.rooms, ...added] });
360
+ await selectRoom(room.id);
361
+ },
362
+ async listModels() {
363
+ const { routes } = await request("collab/api/models");
364
+ return routes;
365
+ },
366
+ async listClis() {
367
+ return await request("collab/api/clis");
368
+ },
369
+ async addMember(input) {
370
+ await request("collab/api/members", json("POST", input));
371
+ },
372
+ async updateMember(id, input) {
373
+ await request(`collab/api/members/${encodeURIComponent(id)}`, json("PATCH", input));
374
+ },
375
+ async removeMember(id) {
376
+ await request(`collab/api/members/${encodeURIComponent(id)}`, { method: "DELETE" });
377
+ },
378
+ async setDispatchMode(roomId, mode) {
379
+ const result = await request(`collab/api/rooms/${roomId}/mode`, json("PATCH", { dispatch_mode: mode }));
380
+ const chatId = snapshot.rooms.find((r) => r.id === roomId)?.parent_id ?? roomId;
381
+ set({ rooms: snapshot.rooms.map((r) => r.id === chatId || r.parent_id === chatId ? {
382
+ ...r,
383
+ ...result
384
+ } : r) });
385
+ },
386
+ async setLead(id) {
387
+ const result = await request("collab/api/lead", json("PATCH", { lead_id: id }));
388
+ if (lifetime !== void 0) onEvent({
389
+ type: "lead",
390
+ lead_id: result.lead_id,
391
+ effective_lead_id: result.effective_lead_id
392
+ });
393
+ },
394
+ async decidePermission(messageId, decision) {
395
+ const owner = lifetime;
396
+ const { message } = await request(`collab/api/messages/${messageId}/permission`, json("POST", { decision }));
397
+ if (lifetime === owner) onEvent({
398
+ type: "message",
399
+ message
400
+ });
401
+ }
402
+ },
403
+ dispose: stop
404
+ };
405
+ }
406
+ // src/client/ChannelsIcon.tsx
407
+ /** Decorative occupant for the channels sidebar entry. */
408
+ /**
409
+ * Render the members glyph at the size the sidebar asks for; the sidebar owns the label.
410
+ * @param props - the sidebar's icon share.
411
+ * @returns decorative icon.
412
+ */
413
+ function ChannelsIcon({ size }) {
414
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconUsersOutlineRegular, { size });
415
+ }
416
+ // src/client/task-board.ts
417
+ /** Locale key of each status label (dsh/collab/tasks.mjs STATUS). */
418
+ const STATUS_KEY = {
419
+ assigned: "taskAssigned",
420
+ accepted: "taskAccepted",
421
+ asked: "taskAsked",
422
+ declined: "taskDeclined",
423
+ escalated: "taskEscalated",
424
+ returned: "taskReturned",
425
+ done: "taskDone",
426
+ rework: "taskRework"
427
+ };
428
+ /** Board order: the owner's turn first (tasks.mjs WORKING), then waiting for the lead, then finished. */
429
+ const GROUPS = [
430
+ [
431
+ "assigned",
432
+ "accepted",
433
+ "asked",
434
+ "rework"
435
+ ],
436
+ [
437
+ "returned",
438
+ "declined",
439
+ "escalated"
440
+ ],
441
+ ["done"]
442
+ ];
443
+ const groupOf = (status) => GROUPS.findIndex((g) => g.includes(status));
444
+ /** Board rows, grouped as above and newest first inside a group. */
445
+ function boardOrder(tasks) {
446
+ return [...tasks].sort((a, b) => groupOf(a.status) - groupOf(b.status) || b.id - a.id);
447
+ }
448
+ /** Seconds a task has taken: until hand-back, until it closed, or until now. */
449
+ function elapsed(task, now) {
450
+ const end = task.returned_at ?? (task.status === "done" ? task.updated_at : now);
451
+ return Math.max(0, end - task.created_at);
452
+ }
453
+ /** Elapsed time as words (unit conversions only). */
454
+ function formatDuration(seconds, t) {
455
+ const s = Math.round(seconds);
456
+ if (s < 60) return t("durSeconds", { n: String(s) });
457
+ if (s < 3600) return t("durMinutes", { n: String(Math.round(s / 60)) });
458
+ return t("durHours", {
459
+ h: String(Math.floor(s / 3600)),
460
+ m: String(Math.round(s % 3600 / 60))
461
+ });
462
+ }
463
+ /** Task-card rows: the task's owner first, then each sub-task in dispatch order. */
464
+ function cardMembers(taskId, tasks) {
465
+ const task = tasks.find((x) => x.id === taskId);
466
+ if (task === void 0) return [];
467
+ return [task, ...tasks.filter((x) => x.parent_task === task.id).sort((a, b) => a.id - b.id)];
468
+ }
469
+ /**
470
+ * The board grouped as chat channel -> workspace -> owner, the shape the owner asked for:
471
+ * a chat channel's board covers all of its workspaces, and inside each block the tasks are
472
+ * listed per member. A task belongs to the room it was dispatched in (its room_id); the
473
+ * board only reads that, it never infers a room from a name.
474
+ * `chatId` is the chat channel the board belongs to, `roomId` the room being viewed: a
475
+ * workspace shows only its own block, the chat channel shows itself and its workspaces.
476
+ * Rooms without tasks are left out; the viewed room itself always stays, so an empty
477
+ * board still says which room it is.
478
+ */
479
+ function boardSections(tasks, rooms, chatId, roomId) {
480
+ if (chatId === void 0) return [];
481
+ const chat = rooms.find((r) => r.id === chatId);
482
+ const viewed = roomId === void 0 ? void 0 : rooms.find((r) => r.id === roomId);
483
+ const blocks = viewed?.kind === "workspace" ? [{
484
+ id: viewed.id,
485
+ name: viewed.name,
486
+ chat: false
487
+ }] : [{
488
+ id: chatId,
489
+ name: chat?.name ?? String(chatId),
490
+ chat: true
491
+ }, ...rooms.filter((r) => r.kind === "workspace" && r.parent_id === chatId).map((r) => ({
492
+ id: r.id,
493
+ name: r.name,
494
+ chat: false
495
+ }))];
496
+ const sections = [];
497
+ for (const block of blocks) {
498
+ const mine = boardOrder(tasks.filter((task) => task.room_id === block.id));
499
+ if (block.id !== roomId && !block.chat && mine.length === 0) continue;
500
+ const byOwner = /* @__PURE__ */ new Map();
501
+ for (const task of mine) {
502
+ let group = byOwner.get(task.owner);
503
+ if (group === void 0) {
504
+ group = {
505
+ open: [],
506
+ done: []
507
+ };
508
+ byOwner.set(task.owner, group);
509
+ }
510
+ (task.status === "done" ? group.done : group.open).push(task);
511
+ }
512
+ const owners = [...byOwner].map(([owner, group]) => ({
513
+ owner,
514
+ ownerName: (group.open[0] ?? group.done[0]).owner_name,
515
+ open: group.open,
516
+ done: group.done
517
+ }));
518
+ sections.push({
519
+ roomId: block.id,
520
+ name: block.name,
521
+ chat: block.chat,
522
+ owners,
523
+ open: owners.reduce((n, g) => n + g.open.length, 0),
524
+ done: owners.reduce((n, g) => n + g.done.length, 0)
525
+ });
526
+ }
527
+ return sections;
528
+ }
529
+ // src/client/ChannelsPage.module.css
530
+ var ChannelsPage_module_default = {
531
+ "activeBusy": "OEONaq_activeBusy",
532
+ "activeCount": "OEONaq_activeCount",
533
+ "addMemberButton": "OEONaq_addMemberButton",
534
+ "author": "OEONaq_author",
535
+ "avatar": "OEONaq_avatar",
536
+ "avatarAnchor": "OEONaq_avatarAnchor",
537
+ "avatarButton": "OEONaq_avatarButton",
538
+ "avatarImage": "OEONaq_avatarImage",
539
+ "avatarMenu": "OEONaq_avatarMenu",
540
+ "avatarMenuItem": "OEONaq_avatarMenuItem",
541
+ "avatarMenuTitle": "OEONaq_avatarMenuTitle",
542
+ "avatarOption": "OEONaq_avatarOption",
543
+ "avatarPicker": "OEONaq_avatarPicker",
544
+ "avatarSmall": "OEONaq_avatarSmall",
545
+ "capabilityHelp": "OEONaq_capabilityHelp",
546
+ "capabilityItem": "OEONaq_capabilityItem",
547
+ "capabilityMenu": "OEONaq_capabilityMenu",
548
+ "card": "OEONaq_card",
549
+ "cardHead": "OEONaq_cardHead",
550
+ "cardTitle": "OEONaq_cardTitle",
551
+ "cardViewButton": "OEONaq_cardViewButton",
552
+ "cardViews": "OEONaq_cardViews",
553
+ "chat": "OEONaq_chat",
554
+ "chatHead": "OEONaq_chatHead",
555
+ "choice": "OEONaq_choice",
556
+ "choiceRow": "OEONaq_choiceRow",
557
+ "composer": "OEONaq_composer",
558
+ "composerRow": "OEONaq_composerRow",
559
+ "dialog": "OEONaq_dialog",
560
+ "dialogActions": "OEONaq_dialogActions",
561
+ "dialogBackdrop": "OEONaq_dialogBackdrop",
562
+ "dialogTitle": "OEONaq_dialogTitle",
563
+ "drawerBackdrop": "OEONaq_drawerBackdrop",
564
+ "drawerOpen": "OEONaq_drawerOpen",
565
+ "empty": "OEONaq_empty",
566
+ "field": "OEONaq_field",
567
+ "fieldInput": "OEONaq_fieldInput",
568
+ "fieldLabel": "OEONaq_fieldLabel",
569
+ "focused": "OEONaq_focused",
570
+ "groupHead": "OEONaq_groupHead",
571
+ "groupTitle": "OEONaq_groupTitle",
572
+ "guide": "OEONaq_guide",
573
+ "hash": "OEONaq_hash",
574
+ "iconButton": "OEONaq_iconButton",
575
+ "input": "OEONaq_input",
576
+ "leadControl": "OEONaq_leadControl",
577
+ "leadMenu": "OEONaq_leadMenu",
578
+ "leadOption": "OEONaq_leadOption",
579
+ "linkButton": "OEONaq_linkButton",
580
+ "log": "OEONaq_log",
581
+ "member": "OEONaq_member",
582
+ "memberActions": "OEONaq_memberActions",
583
+ "memberAvatar": "OEONaq_memberAvatar",
584
+ "memberFailed": "OEONaq_memberFailed",
585
+ "memberModel": "OEONaq_memberModel",
586
+ "memberName": "OEONaq_memberName",
587
+ "memberRow": "OEONaq_memberRow",
588
+ "members": "OEONaq_members",
589
+ "membersHidden": "OEONaq_membersHidden",
590
+ "memberStatus": "OEONaq_memberStatus",
591
+ "memberText": "OEONaq_memberText",
592
+ "mentionActive": "OEONaq_mentionActive",
593
+ "mentionButton": "OEONaq_mentionButton",
594
+ "mentionHint": "OEONaq_mentionHint",
595
+ "mentionItem": "OEONaq_mentionItem",
596
+ "mentionMeta": "OEONaq_mentionMeta",
597
+ "mentionName": "OEONaq_mentionName",
598
+ "mentionPopup": "OEONaq_mentionPopup",
599
+ "mentionTitle": "OEONaq_mentionTitle",
600
+ "message": "OEONaq_message",
601
+ "messageBody": "OEONaq_messageBody",
602
+ "messageHead": "OEONaq_messageHead",
603
+ "mobileButton": "OEONaq_mobileButton",
604
+ "notice": "OEONaq_notice",
605
+ "offline": "OEONaq_offline",
606
+ "page": "OEONaq_page",
607
+ "panelToggle": "OEONaq_panelToggle",
608
+ "panelToggleEnd": "OEONaq_panelToggleEnd",
609
+ "permActions": "OEONaq_permActions",
610
+ "permScope": "OEONaq_permScope",
611
+ "permStatus": "OEONaq_permStatus",
612
+ "presence": "OEONaq_presence",
613
+ "primaryButton": "OEONaq_primaryButton",
614
+ "quote": "OEONaq_quote",
615
+ "quoteAuthor": "OEONaq_quoteAuthor",
616
+ "quoteDeleted": "OEONaq_quoteDeleted",
617
+ "replyBar": "OEONaq_replyBar",
618
+ "replyBarText": "OEONaq_replyBarText",
619
+ "replyButton": "OEONaq_replyButton",
620
+ "replyCancel": "OEONaq_replyCancel",
621
+ "room": "OEONaq_room",
622
+ "roomActive": "OEONaq_roomActive",
623
+ "roomInput": "OEONaq_roomInput",
624
+ "roomLabel": "OEONaq_roomLabel",
625
+ "rooms": "OEONaq_rooms",
626
+ "roomsHidden": "OEONaq_roomsHidden",
627
+ "secondaryButton": "OEONaq_secondaryButton",
628
+ "sendError": "OEONaq_sendError",
629
+ "system": "OEONaq_system",
630
+ "tag": "OEONaq_tag",
631
+ "taskBoard": "OEONaq_taskBoard",
632
+ "taskCard": "OEONaq_taskCard",
633
+ "taskDone": "OEONaq_taskDone",
634
+ "taskEmpty": "OEONaq_taskEmpty",
635
+ "taskLine": "OEONaq_taskLine",
636
+ "taskLinks": "OEONaq_taskLinks",
637
+ "taskList": "OEONaq_taskList",
638
+ "taskMember": "OEONaq_taskMember",
639
+ "taskMemberName": "OEONaq_taskMemberName",
640
+ "taskMembers": "OEONaq_taskMembers",
641
+ "taskMeta": "OEONaq_taskMeta",
642
+ "taskOwnerGroup": "OEONaq_taskOwnerGroup",
643
+ "taskOwnerName": "OEONaq_taskOwnerName",
644
+ "taskRow": "OEONaq_taskRow",
645
+ "taskSection": "OEONaq_taskSection",
646
+ "taskSectionTitle": "OEONaq_taskSectionTitle",
647
+ "taskStatus": "OEONaq_taskStatus",
648
+ "taskSub": "OEONaq_taskSub",
649
+ "taskTitle": "OEONaq_taskTitle",
650
+ "text": "OEONaq_text",
651
+ "time": "OEONaq_time",
652
+ "working": "OEONaq_working",
653
+ "workspace": "OEONaq_workspace"
654
+ };
655
+ // src/client/TaskBoard.tsx
656
+ function clock(seconds) {
657
+ return (/* @__PURE__ */ new Date(seconds * 1e3)).toLocaleTimeString([], {
658
+ hour: "2-digit",
659
+ minute: "2-digit"
660
+ });
661
+ }
662
+ function Status({ status, t }) {
663
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
664
+ className: ChannelsPage_module_default.taskStatus,
665
+ "data-status": status,
666
+ children: t(STATUS_KEY[status])
667
+ });
668
+ }
669
+ /**
670
+ * Right-column board for the selected chat channel.
671
+ * @param onOpen - jump to a message in a room (dispatch or delivery).
672
+ */
673
+ function TaskBoard({ tasks, rooms, chatId, roomId, t, onOpen }) {
674
+ const now = Date.now() / 1e3;
675
+ const sections = boardSections(tasks, rooms, chatId, roomId);
676
+ const openTotal = sections.reduce((n, s) => n + s.open, 0);
677
+ const all = sections.reduce((n, s) => n + s.open + s.done, 0);
678
+ const row = (task) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
679
+ className: ChannelsPage_module_default.taskRow,
680
+ "data-task-id": task.id,
681
+ children: [
682
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
683
+ className: ChannelsPage_module_default.taskLine,
684
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Status, {
685
+ status: task.status,
686
+ t
687
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
688
+ className: ChannelsPage_module_default.taskTitle,
689
+ children: [
690
+ "#",
691
+ task.id,
692
+ " ",
693
+ task.title
694
+ ]
695
+ })]
696
+ }),
697
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
698
+ className: ChannelsPage_module_default.taskMeta,
699
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("taskElapsed", { d: formatDuration(elapsed(task, now), t) }) }), task.next_check_at !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("taskNextCheck", { time: clock(task.next_check_at) }) })]
700
+ }),
701
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
702
+ className: ChannelsPage_module_default.taskLinks,
703
+ children: [task.assign_msg_id !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
704
+ type: "button",
705
+ className: ChannelsPage_module_default.linkButton,
706
+ onClick: () => {
707
+ onOpen(task.room_id, task.assign_msg_id);
708
+ },
709
+ children: t("taskOpenAssign")
710
+ }), task.result_msg_id !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
711
+ type: "button",
712
+ className: ChannelsPage_module_default.linkButton,
713
+ onClick: () => {
714
+ onOpen(task.room_id, task.result_msg_id);
715
+ },
716
+ children: t("taskOpenResult")
717
+ })]
718
+ })
719
+ ]
720
+ }, task.id);
721
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
722
+ className: ChannelsPage_module_default.taskBoard,
723
+ "aria-label": t("taskBoard"),
724
+ children: [
725
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
726
+ className: ChannelsPage_module_default.groupTitle,
727
+ children: t("taskBoardTitle", { count: String(openTotal) })
728
+ }),
729
+ all === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
730
+ className: ChannelsPage_module_default.taskEmpty,
731
+ children: t("taskBoardEmpty")
732
+ }),
733
+ sections.map((section) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
734
+ className: ChannelsPage_module_default.taskSection,
735
+ "data-room-id": section.roomId,
736
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
737
+ className: ChannelsPage_module_default.taskSectionTitle,
738
+ children: section.chat ? t("taskBoardChat") : t("taskBoardWorkspace", { room: section.name })
739
+ }), section.owners.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
740
+ className: ChannelsPage_module_default.taskOwnerGroup,
741
+ "data-owner": group.owner,
742
+ children: [
743
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
744
+ className: ChannelsPage_module_default.taskOwnerName,
745
+ children: t("taskBoardOwner", { name: group.ownerName })
746
+ }),
747
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
748
+ className: ChannelsPage_module_default.taskList,
749
+ children: group.open.map(row)
750
+ }),
751
+ group.done.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
752
+ className: ChannelsPage_module_default.taskDone,
753
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", {
754
+ className: ChannelsPage_module_default.groupTitle,
755
+ children: t("taskDoneGroup", { count: String(group.done.length) })
756
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
757
+ className: ChannelsPage_module_default.taskList,
758
+ children: group.done.map(row)
759
+ })]
760
+ })
761
+ ]
762
+ }, group.owner))]
763
+ }, section.roomId))
764
+ ]
765
+ });
766
+ }
767
+ /**
768
+ * Card for a task record in the message stream: the task and one row per
769
+ * member (owner of the task and of each sub-task) with status and elapsed time.
770
+ * Renders nothing when the task is not on the loaded board.
771
+ */
772
+ function TaskCard({ taskId, tasks, t }) {
773
+ const members = cardMembers(taskId, tasks);
774
+ const task = members[0];
775
+ if (task === void 0) return null;
776
+ const now = Date.now() / 1e3;
777
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
778
+ className: ChannelsPage_module_default.taskCard,
779
+ "data-status": task.status,
780
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
781
+ className: ChannelsPage_module_default.taskLine,
782
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
783
+ className: ChannelsPage_module_default.cardTitle,
784
+ children: [
785
+ "#",
786
+ task.id,
787
+ " ",
788
+ task.title
789
+ ]
790
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Status, {
791
+ status: task.status,
792
+ t
793
+ })]
794
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
795
+ className: ChannelsPage_module_default.taskMembers,
796
+ children: members.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
797
+ className: ChannelsPage_module_default.taskMember,
798
+ children: [
799
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
800
+ className: ChannelsPage_module_default.taskMemberName,
801
+ children: m.owner_name
802
+ }),
803
+ m.id !== task.id && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
804
+ className: ChannelsPage_module_default.taskSub,
805
+ children: m.title
806
+ }),
807
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Status, {
808
+ status: m.status,
809
+ t
810
+ }),
811
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
812
+ className: ChannelsPage_module_default.taskMeta,
813
+ children: formatDuration(elapsed(m, now), t)
814
+ })
815
+ ]
816
+ }, m.id))
817
+ })]
818
+ });
819
+ }
820
+ // src/client/CapabilityList.tsx
821
+ /** On-demand help for existing capabilities; selecting help never executes an action. */
822
+ const CAPABILITIES = [
823
+ ["capabilityTaskBoard", "capabilityTaskBoardHelp"],
824
+ ["capabilityLater", "capabilityLaterHelp"],
825
+ ["capabilityReminder", "capabilityReminderHelp"],
826
+ ["capabilityWorkspace", "capabilityWorkspaceHelp"],
827
+ ["capabilityLead", "capabilityLeadHelp"],
828
+ ["capabilityDm", "capabilityDmHelp"]
829
+ ];
830
+ /** Show the capability names and, after selection, the corresponding usage. */
831
+ function CapabilityList({ t, onClose }) {
832
+ const [selected, setSelected] = (0, react.useState)();
833
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
834
+ className: ChannelsPage_module_default.capabilityMenu,
835
+ "aria-label": t("capabilities"),
836
+ onKeyDown: (event) => {
837
+ if (event.key === "Escape") {
838
+ event.stopPropagation();
839
+ onClose();
840
+ }
841
+ },
842
+ children: [
843
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
844
+ className: ChannelsPage_module_default.groupHead,
845
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("capabilities") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
846
+ type: "button",
847
+ className: ChannelsPage_module_default.iconButton,
848
+ "aria-label": t("close"),
849
+ onClick: onClose,
850
+ children: "×"
851
+ })]
852
+ }),
853
+ CAPABILITIES.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
854
+ type: "button",
855
+ className: ChannelsPage_module_default.capabilityItem,
856
+ "aria-pressed": selected === item,
857
+ onClick: () => {
858
+ setSelected(item);
859
+ },
860
+ children: t(item[0])
861
+ }, item[0])),
862
+ selected !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
863
+ className: ChannelsPage_module_default.capabilityHelp,
864
+ role: "status",
865
+ children: t(selected[1])
866
+ })
867
+ ]
868
+ });
869
+ }
870
+ // src/client/mention-complete.ts
871
+ /** Characters that may sit right before an `@` that starts a mention. */
872
+ const MENTION_BOUNDARY = /[\s([{"'“‘,。、!?:;,.!?:;]/;
873
+ /**
874
+ * The mention being typed at `caret`, if any. A mention starts at an `@` at the
875
+ * beginning of the text or after whitespace/punctuation, and its query runs to
876
+ * the caret without whitespace (so `a@b.com` and finished `@name ` do not open it).
877
+ * @param text - the draft.
878
+ * @param caret - caret offset in `text`.
879
+ * @returns the active mention, or undefined.
880
+ */
881
+ function activeMention(text, caret) {
882
+ const head = text.slice(0, caret);
883
+ const at = head.lastIndexOf("@");
884
+ if (at < 0) return void 0;
885
+ const query = head.slice(at + 1);
886
+ if (/\s/.test(query)) return void 0;
887
+ if (at > 0 && !MENTION_BOUNDARY.test(head[at - 1])) return void 0;
888
+ return {
889
+ start: at,
890
+ query
891
+ };
892
+ }
893
+ /**
894
+ * Members the query can name, best first: agents before people, then prefix
895
+ * matches before substring matches (on name or id, case-insensitive), then
896
+ * online before offline, then name order. An empty query lists everyone.
897
+ * @param members - channel members.
898
+ * @param query - text typed after `@`.
899
+ * @returns ranked candidates.
900
+ */
901
+ function rankMembers(members, query) {
902
+ const q = query.toLowerCase();
903
+ const score = (m) => {
904
+ if (q === "") return 0;
905
+ const name = m.name.toLowerCase();
906
+ const id = m.id.toLowerCase();
907
+ if (name.startsWith(q) || id.startsWith(q)) return 0;
908
+ if (name.includes(q) || id.includes(q)) return 1;
909
+ };
910
+ return members.map((m) => ({
911
+ m,
912
+ s: score(m)
913
+ })).filter((x) => x.s !== void 0).sort((a, b) => Number(a.m.kind === "human") - Number(b.m.kind === "human") || a.s - b.s || Number(!a.m.online) - Number(!b.m.online) || a.m.name.localeCompare(b.m.name)).map((x) => x.m);
914
+ }
915
+ /**
916
+ * Replace the active `@query` with `@name ` and put the caret after it.
917
+ * @param text - the draft.
918
+ * @param caret - caret offset in `text`.
919
+ * @param mention - the active mention at `caret`.
920
+ * @param member - the chosen member.
921
+ * @returns the new draft and caret.
922
+ */
923
+ function applyMention(text, caret, mention, member) {
924
+ const insert = `@${member.name} `;
925
+ const tail = text.slice(caret).replace(/^ /, "");
926
+ return {
927
+ text: text.slice(0, mention.start) + insert + tail,
928
+ caret: mention.start + insert.length
929
+ };
930
+ }
931
+ /**
932
+ * Insert `@name ` at the caret (member-list click), adding a separating space
933
+ * when the caret sits right after a non-space character.
934
+ * @param text - the draft.
935
+ * @param caret - caret offset in `text`.
936
+ * @param member - the member to mention.
937
+ * @returns the new draft and caret.
938
+ */
939
+ function insertMention(text, caret, member) {
940
+ const before = text.slice(0, caret);
941
+ const insert = `${before === "" || /\s$/.test(before) ? "" : " "}@${member.name} `;
942
+ return {
943
+ text: before + insert + text.slice(caret),
944
+ caret: caret + insert.length
945
+ };
946
+ }
947
+ // src/client/avatars.ts
948
+ /**
949
+ * Built-in member avatars served by the collab plugin at `collab/avatars/<slug>.png`:
950
+ * the five original character portraits (user 2026-10-23: 「我还是保留原本那个」).
951
+ */
952
+ const AVATARS = [
953
+ "orange",
954
+ "whale",
955
+ "dragon",
956
+ "rainbow",
957
+ "lanxi"
958
+ ];
959
+ /**
960
+ * Image source for a member avatar.
961
+ * @param avatar - built-in slug, http(s) URL announced by a bot, or empty.
962
+ * @returns a URL relative to the document base, the absolute URL, or undefined for the initial badge.
963
+ */
964
+ function avatarSrc(avatar) {
965
+ if (avatar === void 0 || avatar === "") return void 0;
966
+ if (/^https?:\/\//.test(avatar)) return avatar;
967
+ return AVATARS.includes(avatar) ? `collab/avatars/${avatar}.png` : void 0;
968
+ }
969
+ // src/client/Avatar.tsx
970
+ /** Member portrait: a built-in or bot-announced image over a colored initial badge. */
971
+ const PALETTE = [
972
+ "#5865f2",
973
+ "#eb459e",
974
+ "#3ba55c",
975
+ "#faa61a",
976
+ "#ed4245",
977
+ "#00a8fc",
978
+ "#9b59b6",
979
+ "#e67e22"
980
+ ];
981
+ function color(id) {
982
+ let h = 7;
983
+ for (const c of id) h = h * 31 + c.charCodeAt(0) >>> 0;
984
+ return PALETTE[h % PALETTE.length];
985
+ }
986
+ /** Member portrait, or a colored initial badge when there is none or the image fails to load. */
987
+ function Avatar({ id, name, avatar, small }) {
988
+ const src = avatarSrc(avatar);
989
+ const [broken, setBroken] = (0, react.useState)(void 0);
990
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
991
+ className: small === true ? `${ChannelsPage_module_default.avatar} ${ChannelsPage_module_default.avatarSmall}` : ChannelsPage_module_default.avatar,
992
+ style: { background: color(id) },
993
+ "aria-hidden": true,
994
+ children: [(name || id).slice(0, 1), src !== void 0 && broken !== src && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
995
+ className: ChannelsPage_module_default.avatarImage,
996
+ src,
997
+ alt: "",
998
+ onError: () => {
999
+ setBroken(src);
1000
+ }
1001
+ })]
1002
+ });
1003
+ }
1004
+ // src/client/MemberDialog.tsx
1005
+ /**
1006
+ * Add/edit dialog for one channel member: name, persona, and either a DSH
1007
+ * model (grouped by provider route, as the host lists them) or, when adding, a
1008
+ * CLI agent installed on this machine (Claude Code, Codex, Gemini), plus one of the built-in
1009
+ * avatars or none (initial badge). Submission goes through the
1010
+ * injected action; the host's refusal (name taken, model unavailable) is shown
1011
+ * inline and the form keeps its values.
1012
+ */
1013
+ const SEP = "\0";
1014
+ /** CLI choices offered for a local-CLI member, in display order (user request 2026-10-23). */
1015
+ const CLI_CHOICES = [
1016
+ {
1017
+ cli: "claude",
1018
+ label: "Claude Code"
1019
+ },
1020
+ {
1021
+ cli: "codex",
1022
+ label: "Codex"
1023
+ },
1024
+ {
1025
+ cli: "gemini",
1026
+ label: "Gemini"
1027
+ }
1028
+ ];
1029
+ /**
1030
+ * Render the member dialog.
1031
+ * @param props - target member, actions, and copy.
1032
+ * @returns a modal form.
1033
+ */
1034
+ function MemberDialog({ member, listModels, listClis, onSubmit, onClose, t }) {
1035
+ const [name, setName] = (0, react.useState)(member?.name ?? "");
1036
+ const [persona, setPersona] = (0, react.useState)(member?.persona ?? "");
1037
+ const [modelKey, setModelKey] = (0, react.useState)(member?.model ? member.model.provider + SEP + member.model.model : "");
1038
+ const [routes, setRoutes] = (0, react.useState)(void 0);
1039
+ const [error, setError] = (0, react.useState)(void 0);
1040
+ const [busy, setBusy] = (0, react.useState)(false);
1041
+ const [type, setType] = (0, react.useState)(member?.cli !== void 0 ? "cli" : "dsh");
1042
+ const [clis, setClis] = (0, react.useState)(void 0);
1043
+ const [cli, setCli] = (0, react.useState)(member?.cli ?? "");
1044
+ const [avatar, setAvatar] = (0, react.useState)(member?.avatar !== void 0 && AVATARS.includes(member.avatar) ? member.avatar : "");
1045
+ (0, react.useEffect)(() => {
1046
+ let live = true;
1047
+ listModels().then((list) => {
1048
+ if (live) setRoutes(list);
1049
+ }).catch((reason) => {
1050
+ if (live) {
1051
+ setRoutes([]);
1052
+ setError(reason instanceof Error ? reason.message : String(reason));
1053
+ }
1054
+ });
1055
+ return () => {
1056
+ live = false;
1057
+ };
1058
+ }, [listModels]);
1059
+ const wantClis = type === "cli" && clis === void 0;
1060
+ (0, react.useEffect)(() => {
1061
+ if (!wantClis) return;
1062
+ let live = true;
1063
+ listClis().then((list) => {
1064
+ if (!live) return;
1065
+ setClis(list);
1066
+ setCli((prev) => prev !== "" ? prev : CLI_CHOICES.find((c) => list.some((l) => l.cli === c.cli))?.cli ?? "");
1067
+ }).catch((reason) => {
1068
+ if (live) {
1069
+ setClis([]);
1070
+ setError(reason instanceof Error ? reason.message : String(reason));
1071
+ }
1072
+ });
1073
+ return () => {
1074
+ live = false;
1075
+ };
1076
+ }, [wantClis, listClis]);
1077
+ const submit = (event) => {
1078
+ event.preventDefault();
1079
+ if (busy) return;
1080
+ const [provider, model] = modelKey === "" ? [] : modelKey.split(SEP);
1081
+ setBusy(true);
1082
+ setError(void 0);
1083
+ onSubmit(type === "cli" ? {
1084
+ name: name.trim(),
1085
+ persona,
1086
+ avatar,
1087
+ cli
1088
+ } : {
1089
+ name: name.trim(),
1090
+ persona,
1091
+ avatar,
1092
+ model: provider !== void 0 && model !== void 0 ? {
1093
+ provider,
1094
+ model
1095
+ } : null
1096
+ }).then(onClose).catch((reason) => {
1097
+ setError(reason instanceof Error ? reason.message : String(reason));
1098
+ }).finally(() => {
1099
+ setBusy(false);
1100
+ });
1101
+ };
1102
+ const current = member?.model ? member.model.provider + SEP + member.model.model : void 0;
1103
+ const listed = routes?.some((r) => r.models.some((m) => r.provider + SEP + m.id === current)) ?? false;
1104
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1105
+ className: ChannelsPage_module_default.dialogBackdrop,
1106
+ onMouseDown: (event) => {
1107
+ if (event.target === event.currentTarget) onClose();
1108
+ },
1109
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
1110
+ className: ChannelsPage_module_default.dialog,
1111
+ role: "dialog",
1112
+ "aria-modal": "true",
1113
+ "aria-label": member === void 0 ? t("memberDialogAdd") : t("memberDialogEdit", { name: member.name }),
1114
+ onSubmit: submit,
1115
+ onKeyDown: (event) => {
1116
+ if (event.key === "Escape") onClose();
1117
+ },
1118
+ children: [
1119
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1120
+ className: ChannelsPage_module_default.dialogTitle,
1121
+ children: member === void 0 ? t("memberDialogAdd") : t("memberDialogEdit", { name: member.name })
1122
+ }),
1123
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1124
+ className: ChannelsPage_module_default.field,
1125
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("memberName") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1126
+ className: ChannelsPage_module_default.fieldInput,
1127
+ value: name,
1128
+ maxLength: 32,
1129
+ autoFocus: true,
1130
+ onChange: (event) => {
1131
+ setName(event.target.value);
1132
+ }
1133
+ })]
1134
+ }),
1135
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1136
+ className: ChannelsPage_module_default.field,
1137
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1138
+ id: "member-avatar-label",
1139
+ children: t("memberAvatar")
1140
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1141
+ className: ChannelsPage_module_default.avatarPicker,
1142
+ role: "radiogroup",
1143
+ "aria-labelledby": "member-avatar-label",
1144
+ children: ["", ...AVATARS].map((slug) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1145
+ type: "button",
1146
+ role: "radio",
1147
+ "aria-checked": avatar === slug,
1148
+ className: ChannelsPage_module_default.avatarOption,
1149
+ title: slug === "" ? t("memberAvatarNone") : slug,
1150
+ onClick: () => {
1151
+ setAvatar(slug);
1152
+ },
1153
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
1154
+ id: member?.id ?? (name.trim() || "new"),
1155
+ name: name.trim() || "?",
1156
+ avatar: slug
1157
+ })
1158
+ }, slug === "" ? "none" : slug))
1159
+ })]
1160
+ }),
1161
+ member === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1162
+ className: ChannelsPage_module_default.field,
1163
+ role: "radiogroup",
1164
+ "aria-label": t("memberType"),
1165
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("memberType") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1166
+ className: ChannelsPage_module_default.choiceRow,
1167
+ children: ["dsh", "cli"].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1168
+ className: ChannelsPage_module_default.choice,
1169
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1170
+ type: "radio",
1171
+ name: "member-type",
1172
+ value,
1173
+ checked: type === value,
1174
+ onChange: () => {
1175
+ setType(value);
1176
+ setError(void 0);
1177
+ }
1178
+ }), value === "dsh" ? t("memberTypeDsh") : t("memberTypeCli")]
1179
+ }, value))
1180
+ })]
1181
+ }),
1182
+ type === "cli" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1183
+ className: ChannelsPage_module_default.field,
1184
+ role: "radiogroup",
1185
+ "aria-label": t("memberCli"),
1186
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: clis === void 0 ? t("memberCliLoading") : t("memberCli") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1187
+ className: ChannelsPage_module_default.choiceRow,
1188
+ children: CLI_CHOICES.map(({ cli: id, label }) => {
1189
+ const found = clis?.find((l) => l.cli === id);
1190
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1191
+ className: ChannelsPage_module_default.choice,
1192
+ title: found?.path,
1193
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1194
+ type: "radio",
1195
+ name: "member-cli",
1196
+ value: id,
1197
+ checked: cli === id,
1198
+ disabled: found === void 0,
1199
+ onChange: () => {
1200
+ setCli(id);
1201
+ }
1202
+ }), found === void 0 && clis !== void 0 ? `${label} (${t("memberCliMissing")})` : label]
1203
+ }, id);
1204
+ })
1205
+ })]
1206
+ }),
1207
+ type === "dsh" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1208
+ className: ChannelsPage_module_default.field,
1209
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("memberModel") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1210
+ className: ChannelsPage_module_default.fieldInput,
1211
+ value: modelKey,
1212
+ disabled: routes === void 0,
1213
+ onChange: (event) => {
1214
+ setModelKey(event.target.value);
1215
+ },
1216
+ children: [
1217
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1218
+ value: "",
1219
+ children: routes === void 0 ? t("memberModelLoading") : t("memberModelDefault")
1220
+ }),
1221
+ current !== void 0 && !listed && member?.model && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1222
+ value: current,
1223
+ children: `${member.model.provider}/${member.model.model}`
1224
+ }),
1225
+ routes?.map((route) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("optgroup", {
1226
+ label: route.error === void 0 ? route.name : t("memberModelRouteError", {
1227
+ route: route.name,
1228
+ error: route.error
1229
+ }),
1230
+ children: route.models.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1231
+ value: route.provider + SEP + m.id,
1232
+ children: m.name === m.id ? m.id : `${m.name} (${m.id})`
1233
+ }, m.id))
1234
+ }, route.provider))
1235
+ ]
1236
+ })]
1237
+ }),
1238
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1239
+ className: ChannelsPage_module_default.field,
1240
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("memberPersona") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1241
+ className: ChannelsPage_module_default.fieldInput,
1242
+ rows: 4,
1243
+ value: persona,
1244
+ maxLength: 4e3,
1245
+ onChange: (event) => {
1246
+ setPersona(event.target.value);
1247
+ }
1248
+ })]
1249
+ }),
1250
+ error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1251
+ className: ChannelsPage_module_default.sendError,
1252
+ role: "alert",
1253
+ children: error
1254
+ }),
1255
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1256
+ className: ChannelsPage_module_default.dialogActions,
1257
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1258
+ type: "button",
1259
+ className: ChannelsPage_module_default.secondaryButton,
1260
+ onClick: onClose,
1261
+ children: t("cancel")
1262
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1263
+ type: "submit",
1264
+ className: ChannelsPage_module_default.primaryButton,
1265
+ disabled: busy || name.trim() === "" || type === "cli" && cli === "",
1266
+ children: t("save")
1267
+ })]
1268
+ })
1269
+ ]
1270
+ })
1271
+ });
1272
+ }
1273
+ // src/client/PermissionCard.tsx
1274
+ /**
1275
+ * Buttons for a cloud-web bot's permission prompt (#120). The card JSON is
1276
+ * written by dsh/collab/cloudweb.mjs; the decision goes to
1277
+ * POST collab/api/messages/{id}/permission, and the updated card arrives back
1278
+ * as a `message` event, so the state shown here always comes from the server.
1279
+ */
1280
+ const STATE_TEXT = {
1281
+ allowed: "permAllowed",
1282
+ denied: "permDenied",
1283
+ allowed_all: "permAllowedAll",
1284
+ expired: "permExpired"
1285
+ };
1286
+ function PermissionCard({ messageId, card, t, onDecide }) {
1287
+ const [busy, setBusy] = (0, react.useState)(false);
1288
+ const [error, setError] = (0, react.useState)(void 0);
1289
+ if (card.state !== "pending") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1290
+ className: ChannelsPage_module_default.permStatus,
1291
+ role: "status",
1292
+ children: [t(STATE_TEXT[card.state]), card.decided_by === "text" && ` ${t("permByText")}`]
1293
+ });
1294
+ const decide = (decision) => {
1295
+ if (busy) return;
1296
+ setBusy(true);
1297
+ setError(void 0);
1298
+ onDecide(messageId, decision).catch((e) => {
1299
+ setError(e instanceof Error ? e.message : String(e));
1300
+ }).finally(() => {
1301
+ setBusy(false);
1302
+ });
1303
+ };
1304
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1305
+ className: ChannelsPage_module_default.permActions,
1306
+ children: [
1307
+ card.actions.map((a) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1308
+ type: "button",
1309
+ className: a.decision === "deny" ? ChannelsPage_module_default.secondaryButton : ChannelsPage_module_default.primaryButton,
1310
+ disabled: busy,
1311
+ onClick: () => {
1312
+ decide(a.decision);
1313
+ },
1314
+ children: a.label
1315
+ }, a.decision)),
1316
+ card.actions.some((a) => a.decision === "allow_all") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1317
+ className: ChannelsPage_module_default.permScope,
1318
+ children: t("permAllowAllScope")
1319
+ }),
1320
+ error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1321
+ className: ChannelsPage_module_default.sendError,
1322
+ role: "alert",
1323
+ children: error
1324
+ })
1325
+ ]
1326
+ });
1327
+ }
1328
+ // src/client/ChannelsPage.tsx
1329
+ /**
1330
+ * Channels main panel: room list, message stream with composer, and member
1331
+ * list, in a Discord-style three-column layout. All data arrives through the
1332
+ * injected `useChannels` hook; posting goes through the injected actions.
1333
+ */
1334
+ const CLI_TAG = {
1335
+ claude: "kindClaude",
1336
+ codex: "kindCodex",
1337
+ gemini: "kindGemini"
1338
+ };
1339
+ function KindTag({ kind, cli, t }) {
1340
+ if (cli !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1341
+ className: ChannelsPage_module_default.tag,
1342
+ children: t(CLI_TAG[cli])
1343
+ });
1344
+ if (kind === "dsh") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1345
+ className: ChannelsPage_module_default.tag,
1346
+ children: t("kindDsh")
1347
+ });
1348
+ if (kind === "bot") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1349
+ className: ChannelsPage_module_default.tag,
1350
+ children: t("kindBot")
1351
+ });
1352
+ return null;
1353
+ }
1354
+ /** The quoted original above a reply; a deleted original shows only a placeholder. */
1355
+ function Quote({ quote, t }) {
1356
+ if (quote.deleted === true) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1357
+ className: `${ChannelsPage_module_default.quote} ${ChannelsPage_module_default.quoteDeleted}`,
1358
+ children: t("replyDeleted")
1359
+ });
1360
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1361
+ className: ChannelsPage_module_default.quote,
1362
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1363
+ className: ChannelsPage_module_default.quoteAuthor,
1364
+ children: quote.name
1365
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: quote.text })]
1366
+ });
1367
+ }
1368
+ const CARD_VIEW_DEFAULT = "brief";
1369
+ const CARD_VIEW_KEY = "dsh-channels-card-view:";
1370
+ function loadCardView(roomId) {
1371
+ try {
1372
+ const v = globalThis.localStorage?.getItem(CARD_VIEW_KEY + roomId);
1373
+ if (v === "detail" || v === "brief") return v;
1374
+ } catch (error) {}
1375
+ return CARD_VIEW_DEFAULT;
1376
+ }
1377
+ function saveCardView(roomId, view) {
1378
+ try {
1379
+ globalThis.localStorage?.setItem(CARD_VIEW_KEY + roomId, view);
1380
+ } catch (error) {}
1381
+ }
1382
+ function MessageRow({ message, member, t, onConfigure, menuOpen, onAvatar, onCloseMenu, onMention, onDecide, onReply, tasks, focused, cardView, onCardView }) {
1383
+ let card;
1384
+ if (message.card !== null) try {
1385
+ card = JSON.parse(message.card);
1386
+ } catch (error) {}
1387
+ const classes = [
1388
+ ChannelsPage_module_default.message,
1389
+ message.author_kind === "system" ? ChannelsPage_module_default.system : "",
1390
+ focused ? ChannelsPage_module_default.focused : ""
1391
+ ].filter(Boolean).join(" ");
1392
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1393
+ className: classes,
1394
+ "data-message-id": message.id,
1395
+ children: [
1396
+ member === void 0 || message.author_kind === "system" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
1397
+ id: message.author,
1398
+ name: message.name,
1399
+ avatar: member?.avatar
1400
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1401
+ className: ChannelsPage_module_default.avatarAnchor,
1402
+ "data-avatar-menu": true,
1403
+ onKeyDown: (event) => {
1404
+ if (event.key === "Escape") onCloseMenu();
1405
+ },
1406
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1407
+ type: "button",
1408
+ className: ChannelsPage_module_default.avatarButton,
1409
+ "aria-label": t("memberMenu", { name: member.name }),
1410
+ "aria-haspopup": "menu",
1411
+ "aria-expanded": menuOpen,
1412
+ onClick: onAvatar,
1413
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
1414
+ id: member.id,
1415
+ name: message.name,
1416
+ avatar: member.avatar
1417
+ })
1418
+ }), menuOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1419
+ className: ChannelsPage_module_default.avatarMenu,
1420
+ role: "menu",
1421
+ "aria-label": t("memberMenu", { name: member.name }),
1422
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1423
+ className: ChannelsPage_module_default.avatarMenuTitle,
1424
+ children: member.name
1425
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1426
+ type: "button",
1427
+ role: "menuitem",
1428
+ className: ChannelsPage_module_default.avatarMenuItem,
1429
+ onClick: () => {
1430
+ onMention(member);
1431
+ },
1432
+ children: t("avatarMention")
1433
+ })]
1434
+ })]
1435
+ }),
1436
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1437
+ className: ChannelsPage_module_default.messageBody,
1438
+ children: [
1439
+ message.reply_to !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Quote, {
1440
+ quote: message.reply_to,
1441
+ t
1442
+ }),
1443
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1444
+ className: ChannelsPage_module_default.messageHead,
1445
+ children: [
1446
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1447
+ className: ChannelsPage_module_default.author,
1448
+ children: message.name
1449
+ }),
1450
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KindTag, {
1451
+ kind: member?.kind,
1452
+ cli: member?.cli,
1453
+ t
1454
+ }),
1455
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("time", {
1456
+ className: ChannelsPage_module_default.time,
1457
+ children: (/* @__PURE__ */ new Date(message.created_at * 1e3)).toLocaleString()
1458
+ })
1459
+ ]
1460
+ }),
1461
+ card === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1462
+ className: ChannelsPage_module_default.text,
1463
+ children: message.text
1464
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1465
+ className: ChannelsPage_module_default.card,
1466
+ "data-state": card.state,
1467
+ children: [
1468
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1469
+ className: ChannelsPage_module_default.cardHead,
1470
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1471
+ className: ChannelsPage_module_default.cardTitle,
1472
+ children: card.title
1473
+ }), typeof card.brief === "string" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1474
+ className: ChannelsPage_module_default.cardViews,
1475
+ role: "group",
1476
+ "aria-label": t("cardView"),
1477
+ children: ["detail", "brief"].map((view) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1478
+ type: "button",
1479
+ className: ChannelsPage_module_default.cardViewButton,
1480
+ "aria-pressed": cardView === view,
1481
+ onClick: () => {
1482
+ onCardView(view);
1483
+ },
1484
+ children: t(view === "detail" ? "cardViewDetail" : "cardViewBrief")
1485
+ }, view))
1486
+ })]
1487
+ }),
1488
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1489
+ className: ChannelsPage_module_default.text,
1490
+ children: typeof card.brief === "string" && cardView === "brief" ? card.brief : message.text
1491
+ }),
1492
+ card.action === "configure-model" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1493
+ type: "button",
1494
+ className: ChannelsPage_module_default.secondaryButton,
1495
+ onClick: onConfigure,
1496
+ children: t("configureModel")
1497
+ }),
1498
+ card.kind === "permission" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PermissionCard, {
1499
+ messageId: message.id,
1500
+ card,
1501
+ t,
1502
+ onDecide
1503
+ }),
1504
+ card.kind === "task" && typeof card.task_id === "number" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskCard, {
1505
+ taskId: card.task_id,
1506
+ tasks,
1507
+ t
1508
+ })
1509
+ ]
1510
+ })
1511
+ ]
1512
+ }),
1513
+ message.author_kind !== "system" && card === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1514
+ type: "button",
1515
+ className: ChannelsPage_module_default.replyButton,
1516
+ title: t("reply"),
1517
+ "aria-label": t("reply"),
1518
+ onClick: () => {
1519
+ onReply(message);
1520
+ },
1521
+ children: "↩"
1522
+ })
1523
+ ]
1524
+ });
1525
+ }
1526
+ function MemberGroup({ title, members, busy, onPick, onEdit, onRemove, t }) {
1527
+ if (members.length === 0) return null;
1528
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1529
+ className: ChannelsPage_module_default.groupTitle,
1530
+ children: title
1531
+ }), members.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1532
+ className: `${ChannelsPage_module_default.memberRow} ${m.error ? ChannelsPage_module_default.memberFailed : m.online ? "" : ChannelsPage_module_default.offline}`,
1533
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1534
+ type: "button",
1535
+ className: ChannelsPage_module_default.member,
1536
+ title: t("mentionPick", { name: m.name }),
1537
+ onClick: () => {
1538
+ onPick(m);
1539
+ },
1540
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1541
+ className: ChannelsPage_module_default.memberAvatar,
1542
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
1543
+ id: m.id,
1544
+ name: m.name,
1545
+ avatar: m.avatar,
1546
+ small: true
1547
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: m.error ? `${ChannelsPage_module_default.presence} ${ChannelsPage_module_default.errorPresence}` : ChannelsPage_module_default.presence })]
1548
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1549
+ className: ChannelsPage_module_default.memberText,
1550
+ children: [
1551
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1552
+ className: ChannelsPage_module_default.memberName,
1553
+ children: [m.name, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KindTag, {
1554
+ kind: m.kind,
1555
+ cli: m.cli,
1556
+ t
1557
+ })]
1558
+ }),
1559
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1560
+ className: ChannelsPage_module_default.memberStatus,
1561
+ children: m.error ? t("memberFailed") : busy.has(m.id) ? t("working") : m.online ? t("online") : t("offline")
1562
+ }),
1563
+ m.kind === "dsh" && m.cli === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1564
+ className: ChannelsPage_module_default.memberModel,
1565
+ children: m.model ? `${m.model.provider}/${m.model.model}` : t("defaultModel")
1566
+ })
1567
+ ]
1568
+ })]
1569
+ }), m.kind === "dsh" && (m.editable === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1570
+ className: ChannelsPage_module_default.memberActions,
1571
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1572
+ type: "button",
1573
+ className: ChannelsPage_module_default.linkButton,
1574
+ onClick: () => {
1575
+ onEdit(m);
1576
+ },
1577
+ children: t("editMember")
1578
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1579
+ type: "button",
1580
+ className: ChannelsPage_module_default.linkButton,
1581
+ onClick: () => {
1582
+ onRemove(m);
1583
+ },
1584
+ children: t("removeMember")
1585
+ })]
1586
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1587
+ className: ChannelsPage_module_default.memberActions,
1588
+ title: t("configMember"),
1589
+ children: "⚙"
1590
+ }))]
1591
+ }, m.id))] });
1592
+ }
1593
+ /**
1594
+ * Render the channels panel.
1595
+ * @param props - framework, injected, and locale shares.
1596
+ * @returns the three-column channel view.
1597
+ */
1598
+ function ChannelsPage({ useChannels, selectRoom, send, createRoom, listModels, listClis, addMember, updateMember, removeMember, setLead, setDispatchMode, decidePermission, renderSlot, t }) {
1599
+ const state = useChannels((snapshot) => snapshot);
1600
+ const [drafts, setDrafts] = (0, react.useState)({});
1601
+ const [errors, setErrors] = (0, react.useState)({});
1602
+ const [sending, setSending] = (0, react.useState)({});
1603
+ const [replies, setReplies] = (0, react.useState)({});
1604
+ const pendingRooms = (0, react.useRef)(/* @__PURE__ */ new Set());
1605
+ const draft = state.roomId === void 0 ? "" : drafts[state.roomId] ?? "";
1606
+ const sendError = state.roomId === void 0 ? void 0 : errors[state.roomId];
1607
+ const replying = state.roomId === void 0 ? void 0 : replies[state.roomId];
1608
+ const logRef = (0, react.useRef)(null);
1609
+ const stickRef = (0, react.useRef)(true);
1610
+ const inputRef = (0, react.useRef)(null);
1611
+ const [caret, setCaret] = (0, react.useState)(0);
1612
+ const [dismissedAt, setDismissedAt] = (0, react.useState)(void 0);
1613
+ const [highlight, setHighlight] = (0, react.useState)(0);
1614
+ const pendingCaret = (0, react.useRef)(void 0);
1615
+ const [dialog, setDialog] = (0, react.useState)(void 0);
1616
+ const [modelsOpen, setModelsOpen] = (0, react.useState)(false);
1617
+ const [drawer, setDrawer] = (0, react.useState)(void 0);
1618
+ const [roomsHidden, setRoomsHidden] = (0, react.useState)(false);
1619
+ const [membersHidden, setMembersHidden] = (0, react.useState)(false);
1620
+ const [newRoom, setNewRoom] = (0, react.useState)(void 0);
1621
+ const [roomError, setRoomError] = (0, react.useState)(void 0);
1622
+ const [memberError, setMemberError] = (0, react.useState)(void 0);
1623
+ const [modeSaving, setModeSaving] = (0, react.useState)(false);
1624
+ const [modeError, setModeError] = (0, react.useState)(void 0);
1625
+ const [leadSaving, setLeadSaving] = (0, react.useState)(false);
1626
+ const [leadOpen, setLeadOpen] = (0, react.useState)(false);
1627
+ const [capabilitiesDismissed, setCapabilitiesDismissed] = (0, react.useState)(false);
1628
+ (0, react.useEffect)(() => {
1629
+ setCapabilitiesDismissed(false);
1630
+ }, [state.roomId]);
1631
+ const composerRef = (0, react.useRef)(null);
1632
+ const openMentionAt = (0, react.useRef)(void 0);
1633
+ (0, react.useEffect)(() => {
1634
+ const onPress = (event) => {
1635
+ const at = openMentionAt.current;
1636
+ if (composerRef.current?.contains(event.target) === true) return;
1637
+ if (at !== void 0) setDismissedAt(at);
1638
+ if (draft === "/" && !capabilitiesDismissed) setCapabilitiesDismissed(true);
1639
+ };
1640
+ document.addEventListener("mousedown", onPress);
1641
+ return () => {
1642
+ document.removeEventListener("mousedown", onPress);
1643
+ };
1644
+ }, [draft, capabilitiesDismissed]);
1645
+ (0, react.useEffect)(() => {
1646
+ const input = inputRef.current;
1647
+ const next = pendingCaret.current;
1648
+ if (input === null || next === void 0) return;
1649
+ pendingCaret.current = void 0;
1650
+ input.focus();
1651
+ input.setSelectionRange(next, next);
1652
+ });
1653
+ const [avatarMenu, setAvatarMenu] = (0, react.useState)(void 0);
1654
+ const [cardViews, setCardViews] = (0, react.useState)({});
1655
+ const cardView = state.roomId === void 0 ? CARD_VIEW_DEFAULT : cardViews[state.roomId] ?? loadCardView(state.roomId);
1656
+ const chooseCardView = (view) => {
1657
+ if (state.roomId === void 0) return;
1658
+ const roomId = state.roomId;
1659
+ saveCardView(roomId, view);
1660
+ setCardViews((views) => ({
1661
+ ...views,
1662
+ [roomId]: view
1663
+ }));
1664
+ };
1665
+ (0, react.useEffect)(() => {
1666
+ if (avatarMenu === void 0) return;
1667
+ const onPress = (event) => {
1668
+ if (event.target?.closest?.("[data-avatar-menu]") == null) setAvatarMenu(void 0);
1669
+ };
1670
+ document.addEventListener("mousedown", onPress);
1671
+ return () => {
1672
+ document.removeEventListener("mousedown", onPress);
1673
+ };
1674
+ }, [avatarMenu]);
1675
+ (0, react.useEffect)(() => {
1676
+ const log = logRef.current;
1677
+ if (log !== null && stickRef.current) log.scrollTop = log.scrollHeight;
1678
+ }, [state.messages, state.roomId]);
1679
+ const [jump, setJump] = (0, react.useState)(void 0);
1680
+ const [focusedId, setFocusedId] = (0, react.useState)(void 0);
1681
+ (0, react.useEffect)(() => {
1682
+ if (jump === void 0 || jump.roomId !== state.roomId) return;
1683
+ const el = logRef.current?.querySelector(`[data-message-id="${jump.messageId}"]`);
1684
+ if (el == null) return;
1685
+ stickRef.current = false;
1686
+ el.scrollIntoView?.({ block: "center" });
1687
+ setFocusedId(jump.messageId);
1688
+ setJump(void 0);
1689
+ }, [
1690
+ jump,
1691
+ state.messages,
1692
+ state.roomId
1693
+ ]);
1694
+ const openMessage = (roomId, messageId) => {
1695
+ setDrawer(void 0);
1696
+ setJump({
1697
+ roomId,
1698
+ messageId
1699
+ });
1700
+ if (roomId !== state.roomId) selectRoom(roomId);
1701
+ };
1702
+ if (state.phase !== "ready") {
1703
+ const notice = state.phase === "unavailable" ? t("unavailable") : state.phase === "unauthorized" ? t("unauthorized") : state.phase === "error" ? t("error", { message: state.error ?? "" }) : t("loading");
1704
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1705
+ className: ChannelsPage_module_default.page,
1706
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1707
+ className: ChannelsPage_module_default.notice,
1708
+ role: "status",
1709
+ children: notice
1710
+ })
1711
+ });
1712
+ }
1713
+ const room = state.rooms.find((r) => r.id === state.roomId);
1714
+ const openTaskOwners = new Set((state.tasks ?? []).filter((t) => [
1715
+ "assigned",
1716
+ "accepted",
1717
+ "asked",
1718
+ "rework"
1719
+ ].includes(t.status)).map((t) => t.owner));
1720
+ const busy = /* @__PURE__ */ new Set([...state.roomId === void 0 ? [] : state.thinking[String(state.roomId)] ?? [], ...openTaskOwners]);
1721
+ const byId = new Map(state.members.map((m) => [m.id, m]));
1722
+ const typingNames = (state.roomId === void 0 ? [] : state.thinking[String(state.roomId)] ?? []).map((id) => byId.get(id)?.name ?? id);
1723
+ const mention = activeMention(draft, caret);
1724
+ const candidates = mention === void 0 || mention.start === dismissedAt ? [] : rankMembers(state.members, mention.query);
1725
+ const popupOpen = mention !== void 0 && candidates.length > 0;
1726
+ openMentionAt.current = popupOpen ? mention.start : void 0;
1727
+ const selected = Math.min(highlight, Math.max(candidates.length - 1, 0));
1728
+ const setDraft = (text, nextCaret) => {
1729
+ const roomId = state.roomId;
1730
+ if (roomId === void 0) return;
1731
+ setDrafts((previous) => ({
1732
+ ...previous,
1733
+ [roomId]: text
1734
+ }));
1735
+ setCaret(nextCaret);
1736
+ setHighlight(0);
1737
+ pendingCaret.current = nextCaret;
1738
+ };
1739
+ const choose = (member) => {
1740
+ if (mention === void 0) return;
1741
+ const next = applyMention(draft, caret, mention, member);
1742
+ setDraft(next.text, next.caret);
1743
+ };
1744
+ const pickFromList = (member) => {
1745
+ setDrawer(void 0);
1746
+ const next = insertMention(draft, caret, member);
1747
+ setDraft(next.text, next.caret);
1748
+ };
1749
+ const mentionFromAvatar = (member) => {
1750
+ setAvatarMenu(void 0);
1751
+ pickFromList(member);
1752
+ };
1753
+ const submitRoom = () => {
1754
+ const name = (newRoom ?? "").trim();
1755
+ if (!name) {
1756
+ setNewRoom(void 0);
1757
+ return;
1758
+ }
1759
+ setRoomError(void 0);
1760
+ createRoom(name).then(() => {
1761
+ setNewRoom(void 0);
1762
+ }).catch((error) => {
1763
+ setRoomError(error instanceof Error ? error.message : String(error));
1764
+ });
1765
+ };
1766
+ const confirmRemove = (member) => {
1767
+ if (!window.confirm(t("removeConfirm", { name: member.name }))) return;
1768
+ setMemberError(void 0);
1769
+ removeMember(member.id).catch((error) => {
1770
+ setMemberError(error instanceof Error ? error.message : String(error));
1771
+ });
1772
+ };
1773
+ const changeLead = (id) => {
1774
+ setLeadOpen(false);
1775
+ if (!id || id === state.leadId || leadSaving) return;
1776
+ setMemberError(void 0);
1777
+ setLeadSaving(true);
1778
+ setLead(id).catch((error) => {
1779
+ setMemberError(error instanceof Error ? error.message : String(error));
1780
+ }).finally(() => {
1781
+ setLeadSaving(false);
1782
+ });
1783
+ };
1784
+ const openPicker = () => {
1785
+ const before = draft.slice(0, caret);
1786
+ const insert = before === "" || /\s$/.test(before) ? "@" : " @";
1787
+ setDismissedAt(void 0);
1788
+ setDraft(before + insert + draft.slice(caret), caret + insert.length);
1789
+ };
1790
+ const submit = () => {
1791
+ const roomId = state.roomId;
1792
+ const text = draft.trim();
1793
+ if (!text || roomId === void 0 || pendingRooms.current.has(roomId)) return;
1794
+ pendingRooms.current.add(roomId);
1795
+ setSending((previous) => ({
1796
+ ...previous,
1797
+ [roomId]: true
1798
+ }));
1799
+ setErrors((previous) => ({
1800
+ ...previous,
1801
+ [roomId]: void 0
1802
+ }));
1803
+ stickRef.current = true;
1804
+ const replyTo = replies[roomId]?.id;
1805
+ (replyTo === void 0 ? send(text) : send(text, replyTo)).then(() => {
1806
+ setDrafts((previous) => ({
1807
+ ...previous,
1808
+ [roomId]: ""
1809
+ }));
1810
+ setReplies((previous) => ({
1811
+ ...previous,
1812
+ [roomId]: void 0
1813
+ }));
1814
+ }).catch((error) => {
1815
+ setErrors((previous) => ({
1816
+ ...previous,
1817
+ [roomId]: t("sendFailed", { message: error instanceof Error ? error.message : String(error) })
1818
+ }));
1819
+ }).finally(() => {
1820
+ pendingRooms.current.delete(roomId);
1821
+ setSending((previous) => ({
1822
+ ...previous,
1823
+ [roomId]: false
1824
+ }));
1825
+ });
1826
+ };
1827
+ const startReply = (message) => {
1828
+ const roomId = state.roomId;
1829
+ if (roomId === void 0) return;
1830
+ setReplies((previous) => ({
1831
+ ...previous,
1832
+ [roomId]: message
1833
+ }));
1834
+ pendingCaret.current = caret;
1835
+ };
1836
+ const cancelReply = () => {
1837
+ const roomId = state.roomId;
1838
+ if (roomId !== void 0) setReplies((previous) => ({
1839
+ ...previous,
1840
+ [roomId]: void 0
1841
+ }));
1842
+ };
1843
+ const onKeyDown = (event) => {
1844
+ if (draft === "/" && !capabilitiesDismissed && !event.nativeEvent.isComposing) {
1845
+ if (event.key === "Escape") {
1846
+ event.preventDefault();
1847
+ setCapabilitiesDismissed(true);
1848
+ return;
1849
+ }
1850
+ if (event.key === "Enter") {
1851
+ event.preventDefault();
1852
+ return;
1853
+ }
1854
+ }
1855
+ if (popupOpen && !event.nativeEvent.isComposing) {
1856
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
1857
+ event.preventDefault();
1858
+ const step = event.key === "ArrowDown" ? 1 : -1;
1859
+ setHighlight((selected + step + candidates.length) % candidates.length);
1860
+ return;
1861
+ }
1862
+ if (event.key === "Enter" || event.key === "Tab") {
1863
+ event.preventDefault();
1864
+ choose(candidates[selected]);
1865
+ return;
1866
+ }
1867
+ if (event.key === "Escape") {
1868
+ event.preventDefault();
1869
+ setDismissedAt(mention?.start);
1870
+ return;
1871
+ }
1872
+ }
1873
+ if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
1874
+ event.preventDefault();
1875
+ submit();
1876
+ }
1877
+ };
1878
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1879
+ className: [
1880
+ ChannelsPage_module_default.page,
1881
+ roomsHidden ? ChannelsPage_module_default.roomsHidden : "",
1882
+ membersHidden ? ChannelsPage_module_default.membersHidden : ""
1883
+ ].filter(Boolean).join(" "),
1884
+ onKeyDown: (event) => {
1885
+ if (event.key === "Escape") setDrawer(void 0);
1886
+ },
1887
+ children: [
1888
+ drawer !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1889
+ className: ChannelsPage_module_default.drawerBackdrop,
1890
+ "aria-label": t("closeDrawer"),
1891
+ onClick: () => setDrawer(void 0)
1892
+ }),
1893
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("nav", {
1894
+ className: drawer === "rooms" ? `${ChannelsPage_module_default.rooms} ${ChannelsPage_module_default.drawerOpen}` : ChannelsPage_module_default.rooms,
1895
+ "aria-label": t("rooms"),
1896
+ children: [
1897
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1898
+ className: ChannelsPage_module_default.groupHead,
1899
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1900
+ className: ChannelsPage_module_default.groupTitle,
1901
+ children: t("rooms")
1902
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1903
+ type: "button",
1904
+ className: ChannelsPage_module_default.iconButton,
1905
+ title: t("newRoom"),
1906
+ "aria-label": t("newRoom"),
1907
+ onClick: () => {
1908
+ setNewRoom("");
1909
+ setRoomError(void 0);
1910
+ },
1911
+ children: "+"
1912
+ })]
1913
+ }),
1914
+ newRoom !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1915
+ className: ChannelsPage_module_default.roomInput,
1916
+ autoFocus: true,
1917
+ value: newRoom,
1918
+ maxLength: 32,
1919
+ placeholder: t("newRoomPlaceholder"),
1920
+ "aria-label": t("newRoom"),
1921
+ onChange: (event) => {
1922
+ setNewRoom(event.target.value);
1923
+ },
1924
+ onKeyDown: (event) => {
1925
+ if (event.key === "Enter" && !event.nativeEvent.isComposing) {
1926
+ event.preventDefault();
1927
+ submitRoom();
1928
+ }
1929
+ if (event.key === "Escape") {
1930
+ setNewRoom(void 0);
1931
+ setRoomError(void 0);
1932
+ }
1933
+ },
1934
+ onBlur: () => {
1935
+ if ((newRoom ?? "").trim() === "") setNewRoom(void 0);
1936
+ }
1937
+ }),
1938
+ roomError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1939
+ className: ChannelsPage_module_default.sendError,
1940
+ role: "alert",
1941
+ children: roomError
1942
+ }),
1943
+ groupRooms(state.rooms).map(({ room: r, depth }) => {
1944
+ const active = r.active ?? 0;
1945
+ const showActive = depth === 1 || r.kind === "workspace" || active > 0;
1946
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1947
+ type: "button",
1948
+ className: [
1949
+ ChannelsPage_module_default.room,
1950
+ depth === 1 ? ChannelsPage_module_default.workspace : "",
1951
+ r.id === state.roomId ? ChannelsPage_module_default.roomActive : ""
1952
+ ].filter(Boolean).join(" "),
1953
+ "aria-current": r.id === state.roomId,
1954
+ onClick: () => {
1955
+ stickRef.current = true;
1956
+ setDrawer(void 0);
1957
+ selectRoom(r.id);
1958
+ },
1959
+ children: [
1960
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1961
+ className: ChannelsPage_module_default.hash,
1962
+ "aria-hidden": true,
1963
+ children: "#"
1964
+ }),
1965
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1966
+ className: ChannelsPage_module_default.roomLabel,
1967
+ children: r.name
1968
+ }),
1969
+ showActive && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1970
+ className: active > 0 ? `${ChannelsPage_module_default.activeCount} ${ChannelsPage_module_default.activeBusy}` : ChannelsPage_module_default.activeCount,
1971
+ children: t("activeTasks", { count: active })
1972
+ })
1973
+ ]
1974
+ }, r.id);
1975
+ })
1976
+ ]
1977
+ }),
1978
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1979
+ className: ChannelsPage_module_default.chat,
1980
+ children: [
1981
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
1982
+ className: ChannelsPage_module_default.chatHead,
1983
+ children: [
1984
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1985
+ type: "button",
1986
+ className: ChannelsPage_module_default.mobileButton,
1987
+ "aria-label": t("showRooms"),
1988
+ "aria-expanded": drawer === "rooms",
1989
+ onClick: () => setDrawer(drawer === "rooms" ? void 0 : "rooms"),
1990
+ children: "☰"
1991
+ }),
1992
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1993
+ type: "button",
1994
+ className: ChannelsPage_module_default.panelToggle,
1995
+ title: t(roomsHidden ? "expandRooms" : "collapseRooms"),
1996
+ "aria-label": t(roomsHidden ? "expandRooms" : "collapseRooms"),
1997
+ "aria-expanded": !roomsHidden,
1998
+ onClick: () => {
1999
+ setRoomsHidden(!roomsHidden);
2000
+ },
2001
+ children: roomsHidden ? "»" : "«"
2002
+ }),
2003
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2004
+ className: ChannelsPage_module_default.hash,
2005
+ "aria-hidden": true,
2006
+ children: "#"
2007
+ }),
2008
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2009
+ className: ChannelsPage_module_default.roomName,
2010
+ children: room?.name
2011
+ }),
2012
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2013
+ type: "button",
2014
+ className: `${ChannelsPage_module_default.panelToggle} ${ChannelsPage_module_default.panelToggleEnd}`,
2015
+ title: t(membersHidden ? "expandMembers" : "collapseMembers"),
2016
+ "aria-label": t(membersHidden ? "expandMembers" : "collapseMembers"),
2017
+ "aria-expanded": !membersHidden,
2018
+ onClick: () => {
2019
+ setMembersHidden(!membersHidden);
2020
+ },
2021
+ children: membersHidden ? "«" : "»"
2022
+ }),
2023
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2024
+ type: "button",
2025
+ className: ChannelsPage_module_default.mobileButton,
2026
+ "aria-label": t("showMembers"),
2027
+ "aria-expanded": drawer === "members",
2028
+ onClick: () => setDrawer(drawer === "members" ? void 0 : "members"),
2029
+ children: t("membersButton")
2030
+ })
2031
+ ]
2032
+ }),
2033
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2034
+ ref: logRef,
2035
+ className: ChannelsPage_module_default.log,
2036
+ role: "log",
2037
+ "aria-live": "polite",
2038
+ onScroll: (event) => {
2039
+ const el = event.currentTarget;
2040
+ stickRef.current = el.scrollTop + el.clientHeight >= el.scrollHeight - 40;
2041
+ },
2042
+ children: [!state.members.some((m) => m.kind !== "human") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2043
+ className: ChannelsPage_module_default.guide,
2044
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: t("guideNoMembers") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2045
+ type: "button",
2046
+ className: ChannelsPage_module_default.primaryButton,
2047
+ onClick: () => {
2048
+ setDialog(null);
2049
+ },
2050
+ children: t("addMember")
2051
+ })]
2052
+ }), state.messages.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2053
+ className: ChannelsPage_module_default.empty,
2054
+ children: t("empty")
2055
+ }) : state.messages.map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageRow, {
2056
+ message: m,
2057
+ member: byId.get(m.author),
2058
+ t,
2059
+ onConfigure: () => {
2060
+ setModelsOpen(true);
2061
+ },
2062
+ menuOpen: avatarMenu === m.id,
2063
+ cardView,
2064
+ onCardView: chooseCardView,
2065
+ onAvatar: () => {
2066
+ setAvatarMenu(avatarMenu === m.id ? void 0 : m.id);
2067
+ },
2068
+ onCloseMenu: () => {
2069
+ setAvatarMenu(void 0);
2070
+ },
2071
+ onMention: mentionFromAvatar,
2072
+ onDecide: decidePermission,
2073
+ tasks: state.tasks ?? [],
2074
+ focused: focusedId === m.id,
2075
+ onReply: startReply
2076
+ }, m.id))]
2077
+ }),
2078
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2079
+ className: ChannelsPage_module_default.working,
2080
+ "aria-live": "polite",
2081
+ children: typingNames.length > 0 ? t("workingList", { names: typingNames.join("、") }) : ""
2082
+ }),
2083
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2084
+ className: ChannelsPage_module_default.composer,
2085
+ ref: composerRef,
2086
+ children: [
2087
+ draft === "/" && !capabilitiesDismissed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CapabilityList, {
2088
+ t,
2089
+ onClose: () => {
2090
+ setCapabilitiesDismissed(true);
2091
+ }
2092
+ }, state.roomId),
2093
+ sendError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2094
+ className: ChannelsPage_module_default.sendError,
2095
+ role: "alert",
2096
+ children: sendError
2097
+ }),
2098
+ popupOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2099
+ className: ChannelsPage_module_default.mentionPopup,
2100
+ children: [
2101
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2102
+ className: ChannelsPage_module_default.mentionTitle,
2103
+ children: mention.query === "" ? t("mentionTitle") : t("mentionTitleQuery", { query: mention.query })
2104
+ }),
2105
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2106
+ role: "listbox",
2107
+ id: "channel-mention-list",
2108
+ "aria-label": t("mentionTitle"),
2109
+ children: candidates.map((m, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2110
+ id: `channel-mention-${m.id}`,
2111
+ role: "option",
2112
+ "aria-selected": index === selected,
2113
+ className: index === selected ? `${ChannelsPage_module_default.mentionItem} ${ChannelsPage_module_default.mentionActive}` : ChannelsPage_module_default.mentionItem,
2114
+ onMouseDown: (event) => {
2115
+ event.preventDefault();
2116
+ },
2117
+ onMouseEnter: () => {
2118
+ setHighlight(index);
2119
+ },
2120
+ onClick: () => {
2121
+ choose(m);
2122
+ },
2123
+ children: [
2124
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2125
+ className: ChannelsPage_module_default.memberAvatar,
2126
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Avatar, {
2127
+ id: m.id,
2128
+ name: m.name,
2129
+ avatar: m.avatar,
2130
+ small: true
2131
+ }), m.online && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: m.error ? `${ChannelsPage_module_default.presence} ${ChannelsPage_module_default.errorPresence}` : ChannelsPage_module_default.presence })]
2132
+ }),
2133
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2134
+ className: ChannelsPage_module_default.mentionName,
2135
+ children: m.name
2136
+ }),
2137
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KindTag, {
2138
+ kind: m.kind,
2139
+ cli: m.cli,
2140
+ t
2141
+ }),
2142
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2143
+ className: ChannelsPage_module_default.mentionMeta,
2144
+ children: m.error ? t("memberFailed") : busy.has(m.id) ? t("working") : m.online ? `@${m.id}` : t("offline")
2145
+ })
2146
+ ]
2147
+ }, m.id))
2148
+ }),
2149
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2150
+ className: ChannelsPage_module_default.mentionHint,
2151
+ children: t("mentionHint")
2152
+ })
2153
+ ]
2154
+ }),
2155
+ replying !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2156
+ className: ChannelsPage_module_default.replyBar,
2157
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2158
+ className: ChannelsPage_module_default.replyBarText,
2159
+ children: [t("replyingTo", { name: replying.name }), state.messages.some((m) => m.id === replying.id) ? "" : ` · ${t("replyDeleted")}`]
2160
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2161
+ type: "button",
2162
+ className: ChannelsPage_module_default.replyCancel,
2163
+ title: t("cancelReply"),
2164
+ "aria-label": t("cancelReply"),
2165
+ onClick: cancelReply,
2166
+ children: "×"
2167
+ })]
2168
+ }),
2169
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2170
+ className: ChannelsPage_module_default.composerRow,
2171
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
2172
+ ref: inputRef,
2173
+ className: ChannelsPage_module_default.input,
2174
+ rows: 1,
2175
+ value: draft,
2176
+ disabled: state.roomId === void 0 || sending[state.roomId] === true,
2177
+ placeholder: t("placeholder", { room: room?.name ?? "" }),
2178
+ role: "combobox",
2179
+ "aria-expanded": popupOpen,
2180
+ "aria-controls": "channel-mention-list",
2181
+ "aria-autocomplete": "list",
2182
+ "aria-activedescendant": popupOpen ? `channel-mention-${candidates[selected].id}` : void 0,
2183
+ onChange: (event) => {
2184
+ const roomId = state.roomId;
2185
+ const value = event.target.value;
2186
+ if (roomId !== void 0) setDrafts((previous) => ({
2187
+ ...previous,
2188
+ [roomId]: value
2189
+ }));
2190
+ setCapabilitiesDismissed(false);
2191
+ setCaret(event.target.selectionStart);
2192
+ setHighlight(0);
2193
+ },
2194
+ onSelect: (event) => {
2195
+ setCaret(event.currentTarget.selectionStart);
2196
+ },
2197
+ onKeyDown
2198
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2199
+ type: "button",
2200
+ className: ChannelsPage_module_default.mentionButton,
2201
+ title: t("mentionButton"),
2202
+ "aria-label": t("mentionButton"),
2203
+ disabled: state.roomId === void 0 || sending[state.roomId] === true,
2204
+ onClick: openPicker,
2205
+ children: "@"
2206
+ })]
2207
+ })
2208
+ ]
2209
+ })
2210
+ ]
2211
+ }),
2212
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
2213
+ className: drawer === "members" ? `${ChannelsPage_module_default.members} ${ChannelsPage_module_default.drawerOpen}` : ChannelsPage_module_default.members,
2214
+ children: [
2215
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskBoard, {
2216
+ tasks: state.tasks ?? [],
2217
+ rooms: state.rooms,
2218
+ chatId: state.taskChat,
2219
+ roomId: state.roomId,
2220
+ t,
2221
+ onOpen: openMessage
2222
+ }),
2223
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2224
+ className: ChannelsPage_module_default.leadControl,
2225
+ children: [
2226
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2227
+ className: ChannelsPage_module_default.fieldLabel,
2228
+ children: t("lead")
2229
+ }),
2230
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2231
+ type: "button",
2232
+ className: ChannelsPage_module_default.fieldInput,
2233
+ "aria-haspopup": "listbox",
2234
+ "aria-expanded": leadOpen,
2235
+ "aria-label": t("lead"),
2236
+ disabled: leadSaving || state.members.every((m) => m.kind === "human"),
2237
+ onClick: () => {
2238
+ setLeadOpen(!leadOpen);
2239
+ },
2240
+ children: state.members.find((m) => m.id === (state.effectiveLeadId ?? state.leadId))?.name ?? t("leadNone")
2241
+ }),
2242
+ leadOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2243
+ className: ChannelsPage_module_default.leadMenu,
2244
+ role: "listbox",
2245
+ "aria-label": t("lead"),
2246
+ children: state.members.filter((m) => m.kind !== "human").map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2247
+ type: "button",
2248
+ role: "option",
2249
+ "aria-selected": m.id === state.leadId,
2250
+ className: ChannelsPage_module_default.leadOption,
2251
+ onClick: () => {
2252
+ changeLead(m.id);
2253
+ },
2254
+ children: m.name
2255
+ }, m.id))
2256
+ })
2257
+ ]
2258
+ }),
2259
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
2260
+ disabled: modeSaving || state.roomId === void 0,
2261
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", { children: t("dispatchMode") }), [
2262
+ "solo",
2263
+ "team",
2264
+ "mixed"
2265
+ ].map((mode) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2266
+ style: { display: "block" },
2267
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2268
+ type: "radio",
2269
+ name: "dispatch-mode",
2270
+ value: mode,
2271
+ checked: (state.rooms.find((r) => r.id === state.roomId)?.dispatch_mode ?? "mixed") === mode,
2272
+ onChange: () => {},
2273
+ onClick: () => {
2274
+ const roomId = state.roomId;
2275
+ if (roomId === void 0) return;
2276
+ setModeSaving(true);
2277
+ setModeError(void 0);
2278
+ setDispatchMode(roomId, mode).catch((error) => {
2279
+ setModeError(error instanceof Error ? error.message : String(error));
2280
+ }).finally(() => {
2281
+ setModeSaving(false);
2282
+ });
2283
+ }
2284
+ }), t(mode === "solo" ? "dispatchSolo" : mode === "team" ? "dispatchTeam" : "dispatchMixed")]
2285
+ }, mode))]
2286
+ }),
2287
+ modeError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2288
+ role: "alert",
2289
+ children: modeError
2290
+ }),
2291
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2292
+ type: "button",
2293
+ className: ChannelsPage_module_default.addMemberButton,
2294
+ onClick: () => {
2295
+ setDrawer(void 0);
2296
+ setDialog(null);
2297
+ },
2298
+ children: ["+ ", t("addMember")]
2299
+ }),
2300
+ memberError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2301
+ className: ChannelsPage_module_default.sendError,
2302
+ role: "alert",
2303
+ children: memberError
2304
+ }),
2305
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MemberGroup, {
2306
+ title: t("agents", { count: String(state.members.filter((m) => m.kind !== "human").length) }),
2307
+ members: state.members.filter((m) => m.kind !== "human"),
2308
+ busy,
2309
+ onPick: pickFromList,
2310
+ onEdit: setDialog,
2311
+ onRemove: confirmRemove,
2312
+ t
2313
+ }),
2314
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MemberGroup, {
2315
+ title: t("humans", { count: String(state.members.filter((m) => m.kind === "human").length) }),
2316
+ members: state.members.filter((m) => m.kind === "human"),
2317
+ busy,
2318
+ onPick: pickFromList,
2319
+ onEdit: setDialog,
2320
+ onRemove: confirmRemove,
2321
+ t
2322
+ })
2323
+ ]
2324
+ }),
2325
+ modelsOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2326
+ className: ChannelsPage_module_default.dialogBackdrop,
2327
+ onClick: () => {
2328
+ setModelsOpen(false);
2329
+ },
2330
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2331
+ className: ChannelsPage_module_default.dialog,
2332
+ role: "dialog",
2333
+ "aria-modal": "true",
2334
+ "aria-label": t("configureModel"),
2335
+ onClick: (event) => {
2336
+ event.stopPropagation();
2337
+ },
2338
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2339
+ type: "button",
2340
+ className: ChannelsPage_module_default.secondaryButton,
2341
+ onClick: () => {
2342
+ setModelsOpen(false);
2343
+ },
2344
+ children: t("close")
2345
+ }), renderSlot("settings.section", { close: () => {
2346
+ setModelsOpen(false);
2347
+ } }, { only: "models" })]
2348
+ })
2349
+ }),
2350
+ dialog !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MemberDialog, {
2351
+ member: dialog ?? void 0,
2352
+ listModels,
2353
+ listClis,
2354
+ onSubmit: (input) => dialog === null ? addMember(input) : updateMember(dialog.id, input),
2355
+ onClose: () => {
2356
+ setDialog(void 0);
2357
+ },
2358
+ t
2359
+ })
2360
+ ]
2361
+ });
2362
+ }
2363
+ // src/client/locales.ts
2364
+ /** Channel panel dictionaries. */
2365
+ /** Locale namespace owned by the channel panel. */
2366
+ const NS = "discord-channels";
2367
+ /** Simplified Chinese dictionary and key source. */
2368
+ const zh = {
2369
+ panel: "频道",
2370
+ capabilities: "能力清单",
2371
+ capabilityTaskBoard: "任务板",
2372
+ capabilityLater: "过会儿回来",
2373
+ capabilityReminder: "催办",
2374
+ capabilityWorkspace: "工作区",
2375
+ capabilityLead: "换主角色",
2376
+ capabilityDm: "私信",
2377
+ capabilityTaskBoardHelp: "在右侧任务板查看负责人、状态和下次查看时间,点「派工消息」或「交回消息」回到对应记录。",
2378
+ capabilityLaterHelp: "告诉角色何时回来、回来做什么,例如「十分钟后回来检查测试结果」。",
2379
+ capabilityReminderHelp: "通过任务分发派出的任务,到点会自动催办一次;负责人交回结果或说明卡在哪里。",
2380
+ capabilityWorkspaceHelp: "在聊天频道把任务交给角色,需要时由角色创建工作区。工作区的问题和阶段结果会送回聊天频道。",
2381
+ capabilityLeadHelp: "在成员栏的「谁当主」里选择角色。切换后,没有点名的消息由新的主角色接收。",
2382
+ capabilityDmHelp: "在支持私信的平台打开与角色的私聊。角色也可向配置中的用户发送私信;DSH 频道页目前没有私信入口。",
2383
+ taskBoard: "任务板",
2384
+ taskBoardTitle: "任务板 — 进行中 {count}",
2385
+ taskBoardEmpty: "还没有派出的任务",
2386
+ taskDoneGroup: "已完成 — {count}",
2387
+ taskBoardChat: "聊天频道",
2388
+ taskBoardWorkspace: "{room}",
2389
+ taskBoardOwner: "{name}",
2390
+ taskAssigned: "派出",
2391
+ taskAccepted: "已接",
2392
+ taskAsked: "有疑问",
2393
+ taskDeclined: "退回",
2394
+ taskEscalated: "升级",
2395
+ taskReturned: "交回",
2396
+ taskDone: "完成",
2397
+ taskRework: "返工",
2398
+ taskIn: "在 #{room}",
2399
+ taskElapsed: "用时 {d}",
2400
+ taskNextCheck: "下次看 {time}",
2401
+ taskOpenAssign: "派工消息",
2402
+ taskOpenResult: "交回消息",
2403
+ dispatchMode: "派活方式",
2404
+ dispatchSolo: "单模型多进程 — 同一模型分开处理任务",
2405
+ dispatchTeam: "完全团队 — 交给团队成员协作",
2406
+ dispatchMixed: "混合 — 按任务选择模型与成员",
2407
+ lead: "谁当主",
2408
+ leadNone: "未选择(按原配置)",
2409
+ durSeconds: "{n} 秒",
2410
+ durMinutes: "{n} 分钟",
2411
+ durHours: "{h} 小时 {m} 分",
2412
+ configureModel: "去配置模型",
2413
+ permAllowed: "已允许",
2414
+ permDenied: "已拒绝",
2415
+ permAllowedAll: "已允许(本会话后续请求自动允许)",
2416
+ permExpired: "已过期:这个请求已被新请求取代或 bot 已重启,不再生效",
2417
+ permByText: "(文字回复)",
2418
+ permAllowAllScope: "「允许所有」只对这个 bot 当前会话生效:之后它的工具权限请求自动允许(向你提问的除外),bot 重启或开新会话后恢复逐条询问。",
2419
+ memberFailed: "出错",
2420
+ showRooms: "打开频道列表",
2421
+ showMembers: "打开成员列表",
2422
+ closeDrawer: "关闭列表",
2423
+ membersButton: "成员",
2424
+ collapseRooms: "收起频道列表",
2425
+ expandRooms: "展开频道列表",
2426
+ collapseMembers: "收起成员列表",
2427
+ expandMembers: "展开成员列表",
2428
+ rooms: "文字频道",
2429
+ agents: "Agent — {count}",
2430
+ humans: "真人 — {count}",
2431
+ online: "在线",
2432
+ close: "关闭",
2433
+ offline: "离线",
2434
+ working: "忙",
2435
+ workingList: "{names} 正在输入…",
2436
+ kindDsh: "DSH",
2437
+ kindBot: "Agent",
2438
+ kindClaude: "Claude Code",
2439
+ kindCodex: "Codex",
2440
+ kindGemini: "Gemini",
2441
+ placeholder: "发消息到 #{room},@成员 让它干活(Enter 发送,Shift+Enter 换行)",
2442
+ loading: "正在连接频道服务…",
2443
+ unavailable: "频道服务未启用:在 collab.yml 中配置服务后,用 --patch collab.yml 启动 DSH。",
2444
+ unauthorized: "还没有登录 DSH,请刷新页面。",
2445
+ error: "频道服务出错:{message}",
2446
+ empty: "这里还没有消息。@一个成员开始吧。",
2447
+ guideNoMembers: "频道里还没有角色成员。添加一个:用 DSH 里已配好的模型,或本机已装的 CLI(Claude Code、Codex、Gemini)。",
2448
+ sendFailed: "发送失败:{message}",
2449
+ mentionTitle: "可以 @ 的成员",
2450
+ mentionTitleQuery: "匹配「{query}」的成员",
2451
+ mentionHint: "↑↓ 选择 · Enter / Tab 确认 · Esc 关闭",
2452
+ mentionButton: "@ 一个成员",
2453
+ mentionPick: "在输入框里 @{name}",
2454
+ memberMenu: "{name} 的成员操作",
2455
+ avatarMention: "@ TA",
2456
+ newRoom: "新建频道",
2457
+ newRoomPlaceholder: "频道名,回车创建",
2458
+ activeTasks: "进行中 {count} 件",
2459
+ addMember: "添加成员",
2460
+ editMember: "编辑",
2461
+ removeMember: "移除",
2462
+ removeConfirm: "移除成员「{name}」?历史消息会保留。",
2463
+ memberDialogAdd: "添加 DSH 成员",
2464
+ memberDialogEdit: "编辑成员「{name}」",
2465
+ memberName: "名字(@ 时用,不能有空格)",
2466
+ memberModel: "模型",
2467
+ memberModelDefault: "跟随 DSH 默认模型",
2468
+ memberModelLoading: "正在读取可用模型…",
2469
+ memberModelRouteError: "{route}(读取失败:{error})",
2470
+ memberType: "成员类型",
2471
+ memberTypeDsh: "DSH 模型",
2472
+ memberTypeCli: "本机 CLI",
2473
+ memberCli: "本机 CLI",
2474
+ memberCliLoading: "正在检测本机 CLI…",
2475
+ memberCliMissing: "未检测到",
2476
+ memberPersona: "人设(可选):它是谁、负责什么、说话风格",
2477
+ memberAvatar: "头像",
2478
+ memberAvatarNone: "不用头像(显示首字)",
2479
+ save: "保存",
2480
+ cancel: "取消",
2481
+ configMember: "来自配置文件,网页里不能修改",
2482
+ defaultModel: "默认模型",
2483
+ reply: "回复",
2484
+ cardView: "卡片视图",
2485
+ cardViewDetail: "细节",
2486
+ cardViewBrief: "简略",
2487
+ replyingTo: "回复 {name}",
2488
+ replyDeleted: "原消息已删除",
2489
+ cancelReply: "取消回复"
2490
+ };
2491
+ /** English dictionary. */
2492
+ const en = {
2493
+ dispatchMode: "Dispatch mode",
2494
+ dispatchSolo: "Single model, multiple processes — separate tasks on one model",
2495
+ dispatchTeam: "Full team — delegate to team members",
2496
+ dispatchMixed: "Mixed — choose models and members for each task",
2497
+ taskBoard: "Task board",
2498
+ taskBoardTitle: "Tasks — {count} in progress",
2499
+ taskBoardEmpty: "No tasks dispatched yet",
2500
+ taskDoneGroup: "Done — {count}",
2501
+ taskBoardChat: "Chat channel",
2502
+ taskBoardWorkspace: "{room}",
2503
+ taskBoardOwner: "{name}",
2504
+ taskAssigned: "Assigned",
2505
+ taskAccepted: "Accepted",
2506
+ taskAsked: "Question",
2507
+ taskDeclined: "Declined",
2508
+ taskEscalated: "Escalated",
2509
+ taskReturned: "Handed back",
2510
+ taskDone: "Done",
2511
+ taskRework: "Rework",
2512
+ taskIn: "in #{room}",
2513
+ taskElapsed: "took {d}",
2514
+ taskNextCheck: "next check {time}",
2515
+ taskOpenAssign: "Dispatch",
2516
+ taskOpenResult: "Delivery",
2517
+ lead: "Primary role",
2518
+ leadNone: "No role is available",
2519
+ durSeconds: "{n}s",
2520
+ durMinutes: "{n} min",
2521
+ durHours: "{h} h {m} min",
2522
+ panel: "Channels",
2523
+ capabilities: "Capabilities",
2524
+ capabilityTaskBoard: "Task board",
2525
+ capabilityLater: "Come back later",
2526
+ capabilityReminder: "Reminders",
2527
+ capabilityWorkspace: "Workspaces",
2528
+ capabilityLead: "Primary role",
2529
+ capabilityDm: "Direct messages",
2530
+ capabilityTaskBoardHelp: "View owners, status and next-check times in the task board on the right. Open Dispatch or Delivery to jump to its message.",
2531
+ capabilityLaterHelp: "Tell a role when to return and what to do, for example “Check the test results in ten minutes.”",
2532
+ capabilityReminderHelp: "Dispatched tasks receive one reminder when due. The owner returns a result or explains the blocker.",
2533
+ capabilityWorkspaceHelp: "Assign work to a role in the chat channel. Roles create workspaces when needed; questions and progress return to the chat channel.",
2534
+ capabilityLeadHelp: "Choose a role under Primary role in the members panel. Messages without an explicit addressee then go to the new primary role.",
2535
+ capabilityDmHelp: "Open a private conversation with a role on a supported platform. Roles can also message the configured owner. The DSH Channels page currently has no private-chat entry.",
2536
+ configureModel: "Configure model",
2537
+ permAllowed: "Allowed",
2538
+ permDenied: "Denied",
2539
+ permAllowedAll: "Allowed (later requests in this session are auto-approved)",
2540
+ permExpired: "Expired: replaced by a newer request or the bot restarted",
2541
+ permByText: "(text reply)",
2542
+ permAllowAllScope: "\"Allow all\" applies only to this bot's current session: its later tool permission requests are auto-approved (questions to you excepted) until the bot restarts or starts a new session.",
2543
+ memberFailed: "Error",
2544
+ showRooms: "Open channels",
2545
+ showMembers: "Open members",
2546
+ closeDrawer: "Close list",
2547
+ membersButton: "Members",
2548
+ collapseRooms: "Collapse channels",
2549
+ expandRooms: "Expand channels",
2550
+ collapseMembers: "Collapse members",
2551
+ expandMembers: "Expand members",
2552
+ rooms: "Text channels",
2553
+ agents: "Agents — {count}",
2554
+ humans: "People — {count}",
2555
+ online: "Online",
2556
+ close: "Close",
2557
+ offline: "Offline",
2558
+ working: "Busy",
2559
+ workingList: "{names} typing…",
2560
+ kindDsh: "DSH",
2561
+ kindBot: "Agent",
2562
+ kindClaude: "Claude Code",
2563
+ kindCodex: "Codex",
2564
+ kindGemini: "Gemini",
2565
+ placeholder: "Message #{room}; @ a member to put it to work (Enter sends, Shift+Enter for a new line)",
2566
+ loading: "Connecting to the channel service…",
2567
+ unavailable: "The channel service is off: configure it in collab.yml, then start DSH with --patch collab.yml.",
2568
+ unauthorized: "Not signed in to DSH; reload the page.",
2569
+ error: "Channel service error: {message}",
2570
+ empty: "No messages yet. @ a member to start.",
2571
+ guideNoMembers: "No role members in this channel yet. Add one using a model configured in DSH, or a CLI installed on this machine (Claude Code, Codex, Gemini).",
2572
+ sendFailed: "Send failed: {message}",
2573
+ mentionTitle: "Members you can mention",
2574
+ mentionTitleQuery: "Members matching \"{query}\"",
2575
+ mentionHint: "↑↓ select · Enter / Tab confirm · Esc close",
2576
+ mentionButton: "Mention a member",
2577
+ mentionPick: "Mention {name} in the message box",
2578
+ memberMenu: "Member actions for {name}",
2579
+ avatarMention: "Mention",
2580
+ newRoom: "New channel",
2581
+ newRoomPlaceholder: "Channel name, Enter to create",
2582
+ activeTasks: "{count} in progress",
2583
+ addMember: "Add member",
2584
+ editMember: "Edit",
2585
+ removeMember: "Remove",
2586
+ removeConfirm: "Remove member \"{name}\"? Their messages stay.",
2587
+ memberDialogAdd: "Add a DSH member",
2588
+ memberDialogEdit: "Edit member \"{name}\"",
2589
+ memberName: "Name (used for @, no spaces)",
2590
+ memberModel: "Model",
2591
+ memberModelDefault: "Follow the DSH default model",
2592
+ memberModelLoading: "Loading available models…",
2593
+ memberModelRouteError: "{route} (could not list: {error})",
2594
+ memberType: "Member type",
2595
+ memberTypeDsh: "DSH model",
2596
+ memberTypeCli: "Local CLI",
2597
+ memberCli: "Local CLI",
2598
+ memberCliLoading: "Detecting CLIs on this machine…",
2599
+ memberCliMissing: "not found",
2600
+ memberPersona: "Persona (optional): who it is, what it does, how it talks",
2601
+ memberAvatar: "Avatar",
2602
+ memberAvatarNone: "No avatar (show initial)",
2603
+ save: "Save",
2604
+ cancel: "Cancel",
2605
+ configMember: "From the config file; not editable here",
2606
+ defaultModel: "Default model",
2607
+ reply: "Reply",
2608
+ cardView: "Card view",
2609
+ cardViewDetail: "Detail",
2610
+ cardViewBrief: "Brief",
2611
+ replyingTo: "Replying to {name}",
2612
+ replyDeleted: "Original message was deleted",
2613
+ cancelReply: "Cancel reply"
2614
+ };
2615
+ // src/client/index.ts
2616
+ const PANEL_ID = "channels";
2617
+ /** Required browser services. */
2618
+ const inject = [
2619
+ "slots",
2620
+ "locale",
2621
+ "layout"
2622
+ ];
2623
+ /**
2624
+ * Register the channel dictionaries, the main panel, and its sidebar entry.
2625
+ * @param ctx - Client root context.
2626
+ */
2627
+ function apply(ctx) {
2628
+ ctx.effect(() => ctx.locale.register(NS, {
2629
+ zh,
2630
+ en
2631
+ }), "client-ui-discord: dictionaries");
2632
+ const t = ctx.locale.bind(NS);
2633
+ const channels = createChannelSource(browserTransport());
2634
+ ctx.effect(() => channels.dispose, "client-ui-discord: channel stream");
2635
+ const injected = () => ({
2636
+ hooks: { channels: channels.source },
2637
+ selectRoom: channels.actions.selectRoom,
2638
+ send: channels.actions.send,
2639
+ createRoom: channels.actions.createRoom,
2640
+ listModels: channels.actions.listModels,
2641
+ listClis: channels.actions.listClis,
2642
+ addMember: channels.actions.addMember,
2643
+ updateMember: channels.actions.updateMember,
2644
+ removeMember: channels.actions.removeMember,
2645
+ setDispatchMode: channels.actions.setDispatchMode,
2646
+ setLead: channels.actions.setLead,
2647
+ decidePermission: channels.actions.decidePermission
2648
+ });
2649
+ ctx.slots.inject("main", () => {
2650
+ const dispose = ctx.slots.register({
2651
+ name: "main",
2652
+ key: PANEL_ID,
2653
+ locale: NS,
2654
+ inject: injected,
2655
+ children: { "settings.section": {
2656
+ kind: "list",
2657
+ scope: "root"
2658
+ } }
2659
+ }, ChannelsPage);
2660
+ if (location.hash === `#${PANEL_ID}`) ctx.layout.selectPanel(PANEL_ID);
2661
+ return dispose;
2662
+ });
2663
+ ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
2664
+ name: "sidebar.panellist",
2665
+ id: PANEL_ID,
2666
+ order: 5,
2667
+ locale: NS,
2668
+ label: () => t("panel")
2669
+ }, ChannelsIcon));
2670
+ }
2671
+ exports.apply = apply;
2672
+ exports.inject = inject;
2673
+
2674
+ return module.exports;
2675
+ }
2676
+ });