bothouse-dsh 0.1.42 → 0.1.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client/client.js CHANGED
@@ -1,11 +1,11 @@
1
- // dsh-client-source-sha256: 965ccf38af474ab437c2d687a2673a7ffd705c307462a422262d0341a0f9f35d
2
- // dsh-style-sha256: 47b65ba1c036073fb41c713749a1f3e0efdf2939053123d1baa3ac21e0d821bf
1
+ // dsh-client-source-sha256: 51732729e2bd1e81bb4d9c84a793292ba99e9ca0bbed95f72393ce083fbaeadd
2
+ // dsh-style-sha256: 8de64f320b70b31c6dba515e74187a98284317258d9cb57df3dc16f2eb345641
3
3
  window.__ModuleLoader__.load({
4
4
  id: "bothouse-dsh",
5
5
  factory: (require) => {
6
6
  var module = { exports: {} };
7
7
  var exports = module.exports;
8
- (function(){var s=document.createElement('style');s.setAttribute('data-dsh-plugin',"bothouse-dsh");s.textContent=".dshc_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.dshc_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.dshc_rooms, .dshc_members {\n background: var(--dsw-alias-bg-layer-2);\n min-height: 0;\n padding: 0 8px 16px;\n overflow-y: auto;\n}\n\n.dshc_membersHead {\n border-bottom: 1px solid var(--dsw-alias-border-l2);\n flex: none;\n height: 44px;\n margin: 0 0 8px;\n}\n\n.dshc_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.dshc_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.dshc_room:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_room:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color);\n}\n\n.dshc_workspace {\n padding-left: 22px;\n font-size: .95em;\n}\n\n.dshc_roomLabel {\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.dshc_activeCount {\n color: var(--dsw-alias-label-tertiary);\n flex-shrink: 0;\n margin-left: auto;\n font-size: 11px;\n}\n\n.dshc_activeBusy {\n color: var(--dsw-alias-label-primary);\n}\n\n.dshc_roomActive {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_hash {\n color: var(--dsw-alias-label-tertiary);\n font-size: 16px;\n}\n\n.dshc_chat {\n flex-direction: column;\n min-width: 0;\n min-height: 0;\n display: flex;\n}\n\n.dshc_chatHead {\n -webkit-app-region: drag;\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.dshc_log {\n flex: 1;\n min-height: 0;\n padding: 8px 0 12px;\n overflow-y: auto;\n}\n\n.dshc_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.dshc_empty {\n color: var(--dsw-alias-label-tertiary);\n padding: 24px 16px;\n}\n\n.dshc_message {\n grid-template-columns: 36px minmax(0, 1fr);\n gap: 12px;\n padding: 6px 16px;\n display: grid;\n}\n\n.dshc_message:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_system .dshc_text {\n color: var(--dsw-alias-label-tertiary);\n font-style: italic;\n}\n\n.dshc_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.dshc_avatarImage {\n object-fit: cover;\n border-radius: 50%;\n width: 100%;\n height: 100%;\n position: absolute;\n inset: 0;\n}\n\n.dshc_avatarSmall {\n width: 28px;\n height: 28px;\n font-size: 12px;\n}\n\n.dshc_avatarAnchor {\n align-self: start;\n position: relative;\n}\n\n.dshc_avatarButton {\n cursor: pointer;\n background: none;\n border: 0;\n border-radius: 50%;\n padding: 0;\n}\n\n.dshc_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.dshc_avatarMenuTitle {\n padding: 4px 8px;\n font-weight: 700;\n}\n\n.dshc_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.dshc_avatarMenuItem:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_messageBody {\n min-width: 0;\n}\n\n.dshc_messageHead {\n align-items: baseline;\n gap: 6px;\n display: flex;\n}\n\n.dshc_author {\n font-weight: 600;\n}\n\n.dshc_time {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n}\n\n.dshc_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.dshc_edited {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n}\n\n.dshc_ownActions {\n margin-left: auto;\n margin-right: var(--channels-reply-clearance);\n opacity: 0;\n gap: 8px;\n display: inline-flex;\n}\n\n.dshc_message:hover .dshc_ownActions, .dshc_ownActions:focus-within {\n opacity: 1;\n}\n\n@media (hover: none) {\n .dshc_ownActions {\n opacity: 1;\n }\n}\n\n.dshc_editBox {\n flex-direction: column;\n gap: 6px;\n margin-top: 4px;\n display: flex;\n}\n\n.dshc_editInput {\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-height: 60px;\n color: inherit;\n font: inherit;\n resize: vertical;\n padding: 8px;\n}\n\n.dshc_editBar {\n justify-content: flex-end;\n align-items: center;\n gap: 8px;\n display: flex;\n}\n\n.dshc_editBar .dshc_time {\n margin-right: auto;\n}\n\n.dshc_text {\n white-space: normal;\n overflow-wrap: anywhere;\n}\n\n.dshc_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.dshc_card[data-state=\"failed\"] {\n border-left-color: var(--dsw-alias-state-error-primary);\n}\n\n.dshc_card[data-state=\"retrying\"] {\n border-left-color: var(--dsw-alias-state-warning-primary, #faa61a);\n}\n\n.dshc_cardButtons {\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n margin-top: 6px;\n display: flex;\n}\n\n.dshc_cardTitle {\n font-size: 13px;\n font-weight: 600;\n}\n\n.dshc_cardHead {\n justify-content: space-between;\n align-items: center;\n gap: 8px;\n display: flex;\n}\n\n.dshc_cardViews {\n gap: 4px;\n display: flex;\n}\n\n.dshc_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.dshc_cardViewButton[aria-pressed=\"true\"] {\n background: var(--dsw-alias-bg-base);\n font-weight: 600;\n}\n\n.dshc_permActions {\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n margin-top: 6px;\n display: flex;\n}\n\n.dshc_permScope {\n color: var(--dsw-alias-text-secondary);\n flex-basis: 100%;\n font-size: 12px;\n}\n\n.dshc_permStatus {\n margin-top: 6px;\n font-size: 13px;\n font-weight: 600;\n}\n\n.dshc_message {\n --channels-reply-clearance: 36px;\n position: relative;\n}\n\n.dshc_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.dshc_quoteDeleted {\n font-style: italic;\n}\n\n.dshc_quoteAuthor {\n flex: none;\n font-weight: 600;\n}\n\n.dshc_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.dshc_message:hover .dshc_replyButton, .dshc_message:focus-within .dshc_replyButton, .dshc_replyButton:focus-visible {\n opacity: 1;\n}\n\n@media (hover: none) {\n .dshc_replyButton {\n opacity: 1;\n }\n}\n\n.dshc_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.dshc_replyBarText {\n white-space: nowrap;\n text-overflow: ellipsis;\n flex: 1;\n min-width: 0;\n overflow: hidden;\n}\n\n.dshc_replyCancel {\n color: inherit;\n cursor: pointer;\n background: none;\n border: 0;\n flex: none;\n}\n\n.dshc_working {\n min-height: 20px;\n color: var(--dsw-alias-label-tertiary);\n padding: 0 16px;\n font-size: 12px;\n}\n\n.dshc_composer {\n padding: 0 16px 16px;\n position: relative;\n}\n\n.dshc_composerBox {\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: var(--dsw-radius-xl);\n background: var(--dsw-alias-bg-layer-1, var(--dsw-alias-bg-layer-2));\n box-shadow: var(--dsw-shadow-lv1-blur);\n flex-direction: column;\n display: flex;\n overflow: hidden;\n}\n\n.dshc_composerBox:focus-within {\n border-color: var(--dsw-alias-label-tertiary);\n}\n\n.dshc_composerToolbar {\n flex: none;\n align-items: center;\n gap: 4px;\n padding: 4px 10px 10px;\n display: flex;\n}\n\n.dshc_composerBox > .dshc_replyBar, .dshc_composerBox > .dshc_attachments {\n margin: 10px 14px 0;\n}\n\n.dshc_mentionButton {\n border-radius: var(--dsw-radius-full, 50%);\n width: 32px;\n height: 32px;\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n cursor: pointer;\n background: none;\n border: 0;\n flex: none;\n justify-content: center;\n align-items: center;\n padding: 0;\n font-size: 18px;\n display: inline-flex;\n}\n\n.dshc_mentionButton:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_sendButton {\n border-radius: var(--dsw-radius-full, 50%);\n width: 32px;\n height: 32px;\n color: var(--dsw-alias-label-primary-inverted);\n background: var(--dsw-alias-label-primary);\n font: inherit;\n cursor: pointer;\n border: 0;\n flex: none;\n justify-content: center;\n align-items: center;\n margin-left: auto;\n padding: 0;\n display: inline-flex;\n}\n\n.dshc_sendButton:disabled {\n opacity: .45;\n cursor: default;\n}\n\n.dshc_sendButton:focus-visible, .dshc_mentionButton:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color);\n}\n\n.dshc_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.dshc_mentionPopup > [role=\"listbox\"] {\n min-height: 0;\n overflow-y: auto;\n}\n\n.dshc_mentionTitle {\n color: var(--dsw-alias-label-tertiary);\n padding: 4px 8px 6px;\n font-size: 12px;\n font-weight: 600;\n}\n\n.dshc_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.dshc_mentionActive {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_mentionName {\n font-weight: 600;\n}\n\n.dshc_mentionMeta {\n color: var(--dsw-alias-label-tertiary);\n margin-left: auto;\n font-size: 12px;\n}\n\n.dshc_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.dshc_sendError {\n color: var(--dsw-alias-state-error-primary);\n margin-bottom: 6px;\n font-size: 12px;\n}\n\n.dshc_input {\n box-sizing: border-box;\n width: 100%;\n min-height: 52px;\n max-height: 40vh;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n resize: none;\n field-sizing: content;\n background: none;\n border: 0;\n border-radius: 0;\n flex: 1;\n min-width: 0;\n padding: 14px 16px 8px;\n line-height: 1.5;\n}\n\n.dshc_input::placeholder {\n white-space: nowrap;\n text-overflow: ellipsis;\n overflow: hidden;\n}\n\n@media (width <= 600px) {\n .dshc_composer {\n padding: 0 8px 8px;\n }\n\n .dshc_composerToolbar {\n padding: 4px 8px 8px;\n }\n\n .dshc_input {\n padding: 12px 14px 8px;\n }\n}\n\n.dshc_input:focus-visible {\n outline: none;\n}\n\n.dshc_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.dshc_member:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_member:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color);\n}\n\n.dshc_offline {\n opacity: .5;\n}\n\n.dshc_memberAvatar {\n position: relative;\n}\n\n.dshc_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.dshc_offline .dshc_presence {\n background: var(--dsw-alias-label-tertiary);\n}\n\n.dshc_memberText {\n flex-direction: column;\n min-width: 0;\n display: flex;\n}\n\n.dshc_memberName {\n align-items: center;\n gap: 6px;\n display: flex;\n}\n\n.dshc_memberStatus {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n}\n\n@media (width <= 900px) {\n .dshc_page {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .dshc_rooms, .dshc_members {\n display: none;\n }\n}\n\n.dshc_groupHead {\n justify-content: space-between;\n align-items: center;\n padding-right: 4px;\n display: flex;\n}\n\n.dshc_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.dshc_iconButton:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_roomInput, .dshc_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.dshc_roomInput {\n margin: 2px 0 6px;\n}\n\n.dshc_roomInput:focus-visible, .dshc_fieldInput:focus-visible, .dshc_iconButton:focus-visible, .dshc_linkButton:focus-visible, .dshc_addMemberButton:focus-visible, .dshc_primaryButton:focus-visible, .dshc_secondaryButton:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color);\n}\n\n.dshc_addMemberButton {\n box-sizing: border-box;\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.dshc_addMemberButton:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_settingsDivider {\n border: 0;\n border-top: 1px solid var(--dsw-alias-border-l1);\n width: 100%;\n margin: 8px 0 4px;\n}\n\n.dshc_memberListDivider {\n border: 0;\n border-top: 1px solid var(--dsw-alias-border-l1);\n width: calc(100% - 16px);\n margin: 12px 8px 2px;\n}\n\n.dshc_settingsButton {\n text-align: left;\n border-style: solid;\n border-color: #0000;\n align-items: center;\n gap: 8px;\n display: flex;\n}\n\n.dshc_settingsButton svg {\n flex: none;\n}\n\n.dshc_memberRow {\n border-radius: var(--dsw-radius-sm);\n align-items: center;\n display: flex;\n position: relative;\n}\n\n.dshc_memberRow:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_memberRow .dshc_member:hover {\n background: none;\n}\n\n.dshc_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.dshc_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.dshc_memberRow:hover .dshc_memberActions, .dshc_memberRow:focus-within .dshc_memberActions {\n opacity: 1;\n}\n\n.dshc_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.dshc_linkButton:hover {\n color: var(--dsw-alias-label-primary);\n text-decoration: underline;\n}\n\n.dshc_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.dshc_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.dshc_settingsDialog {\n box-sizing: border-box;\n height: min(70vh, 100dvh - 32px);\n max-height: calc(100dvh - 32px);\n overflow: hidden;\n}\n\n.dshc_settingsBody {\n overscroll-behavior: contain;\n flex: auto;\n min-height: 0;\n padding: 2px;\n overflow-y: auto;\n}\n\n.dshc_settingsDialog > :not(.dshc_settingsBody) {\n flex-shrink: 0;\n}\n\n.dshc_settingsFields {\n flex-direction: column;\n gap: 12px;\n display: flex;\n}\n\n.dshc_settingsFeedback {\n overflow-wrap: anywhere;\n max-height: 20%;\n overflow-y: auto;\n}\n\n.dshc_settingsDialog .dshc_dialogActions {\n justify-content: space-between;\n}\n\n@media (width <= 599px) {\n .dshc_settingsDialog {\n height: calc(100dvh - 32px);\n padding: 16px;\n }\n}\n\n.dshc_avatarCropStage {\n background: #000000a6;\n padding: 20px;\n}\n\n.dshc_avatarCropFrame {\n aspect-ratio: 1;\n touch-action: none;\n cursor: grab;\n outline: 2px solid #fff;\n width: min(256px, 100%);\n margin: auto;\n position: relative;\n overflow: hidden;\n}\n\n.dshc_avatarCropFrame:active {\n cursor: grabbing;\n}\n\n.dshc_avatarCropFrame img {\n pointer-events: none;\n user-select: none;\n max-width: none;\n position: absolute;\n}\n\n.dshc_dialogTitle {\n margin: 0;\n font-size: 16px;\n}\n\n.dshc_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.dshc_field textarea {\n resize: vertical;\n}\n\n.dshc_choiceRow {\n flex-wrap: wrap;\n gap: 12px;\n display: flex;\n}\n\n.dshc_choiceColumn {\n flex-direction: column;\n gap: 12px;\n display: flex;\n}\n\n.dshc_choiceColumn .dshc_choice {\n grid-template-columns: max-content minmax(0, 1fr);\n align-items: start;\n display: grid;\n}\n\n.dshc_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.dshc_dialogActions {\n justify-content: flex-end;\n gap: 8px;\n display: flex;\n}\n\n.dshc_primaryButton, .dshc_secondaryButton {\n border-radius: var(--dsw-radius-sm);\n font: inherit;\n cursor: pointer;\n padding: 7px 14px;\n}\n\n.dshc_primaryButton {\n color: #fff;\n background: var(--dsw-alias-state-business-primary);\n border: 0;\n}\n\n.dshc_primaryButton:disabled {\n cursor: not-allowed;\n opacity: .5;\n}\n\n.dshc_secondaryButton {\n border: 1px solid var(--dsw-alias-border-l2);\n color: var(--dsw-alias-label-primary);\n background: none;\n}\n\n.dshc_memberFailed .dshc_memberStatus {\n color: var(--dsw-alias-state-error-primary, #ed4245);\n}\n\n.dshc_memberFailed .dshc_presence {\n background: #ed4245;\n}\n\n.dshc_mobileButton, .dshc_drawerBackdrop {\n display: none;\n}\n\n.dshc_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.dshc_panelToggle:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_panelToggle:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color);\n}\n\n.dshc_panelToggleEnd {\n margin-left: auto;\n}\n\n.dshc_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.dshc_capabilityItem:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_capabilityHelp {\n color: var(--dsw-alias-label-secondary);\n white-space: normal;\n margin: 6px;\n font-size: 12px;\n}\n\n.dshc_capabilityItem:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color);\n}\n\n.dshc_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 .dshc_page.dshc_roomsHidden {\n grid-template-columns: minmax(0, 1fr) 240px;\n }\n\n .dshc_page.dshc_membersHidden {\n grid-template-columns: 220px minmax(0, 1fr);\n }\n\n .dshc_page.dshc_roomsHidden.dshc_membersHidden {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .dshc_roomsHidden > .dshc_rooms, .dshc_membersHidden > .dshc_members {\n display: none;\n }\n}\n\n@media (width <= 900px) {\n .dshc_page {\n position: relative;\n overflow: hidden;\n }\n\n .dshc_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 .dshc_mobileButton:last-child {\n margin-left: auto;\n }\n\n .dshc_panelToggle {\n display: none;\n }\n\n .dshc_drawerBackdrop {\n z-index: 4;\n background: #00000073;\n border: 0;\n display: block;\n position: absolute;\n inset: 0;\n }\n\n .dshc_rooms.dshc_drawerOpen, .dshc_members.dshc_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 .dshc_rooms.dshc_drawerOpen {\n left: 0;\n }\n\n .dshc_members.dshc_drawerOpen {\n right: 0;\n }\n\n .dshc_memberActions {\n opacity: 1;\n }\n\n .dshc_messageHead {\n flex-wrap: wrap;\n }\n}\n\n.dshc_avatarPicker {\n flex-wrap: wrap;\n gap: 8px;\n display: flex;\n}\n\n.dshc_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.dshc_avatarOption[aria-checked=\"true\"] {\n border-color: var(--dsw-alias-accent, #5865f2);\n}\n\n.dshc_taskBoardTrigger {\n box-sizing: border-box;\n border-radius: var(--dsw-radius-sm);\n width: 100%;\n height: 34px;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n text-align: left;\n cursor: pointer;\n background: none;\n border: 0;\n flex: none;\n align-items: center;\n gap: 8px;\n margin: 4px 0;\n padding: 7px 8px;\n font-size: 12px;\n display: flex;\n}\n\n.dshc_taskBoardTrigger:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_taskBoardTrigger:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color);\n}\n\n.dshc_taskBoardSummary {\n color: var(--dsw-alias-label-secondary);\n text-align: right;\n text-overflow: ellipsis;\n white-space: nowrap;\n flex: 1;\n overflow: hidden;\n}\n\n.dshc_taskBoardDialog {\n width: min(920px, 100%);\n height: min(70vh, 100dvh - 32px);\n overflow: hidden;\n}\n\n.dshc_taskBoardBody {\n overscroll-behavior: contain;\n flex: auto;\n min-height: 0;\n overflow-y: auto;\n}\n\n.dshc_taskBoardDialog > :not(.dshc_taskBoardBody) {\n flex-shrink: 0;\n}\n\n.dshc_boardTabs {\n border-bottom: 1px solid var(--dsw-alias-border-l2);\n gap: 4px;\n padding-bottom: 8px;\n display: flex;\n overflow-x: auto;\n}\n\n.dshc_boardTabs button {\n border-radius: var(--dsw-radius-sm);\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n cursor: pointer;\n background: none;\n border: 0;\n flex: none;\n padding: 7px 10px;\n font-size: 13px;\n}\n\n.dshc_boardTabs button:hover, .dshc_boardTabs button[aria-selected=\"true\"] {\n color: var(--dsw-alias-state-business-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_boardTabs button:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color);\n outline-offset: -2px;\n}\n\n.dshc_boardTabCount {\n font-variant-numeric: tabular-nums;\n margin-left: 4px;\n}\n\n.dshc_boardCardGrid {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 12px;\n margin: 6px 0 16px;\n padding: 2px;\n list-style: none;\n display: grid;\n}\n\n.dshc_boardTaskCard {\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-base);\n border-radius: 12px;\n flex-direction: column;\n gap: 10px;\n min-width: 0;\n padding: 14px;\n transition: transform .14s, box-shadow .14s;\n display: flex;\n}\n\n.dshc_boardTaskCard[data-working=\"true\"] {\n border-color: var(--dsw-alias-state-business-primary);\n}\n\n.dshc_boardTaskCard[data-status=\"done\"] {\n filter: saturate(.25);\n color: var(--dsw-alias-label-secondary);\n}\n\n.dshc_boardTaskCard:hover {\n transform: translateY(-2px);\n box-shadow: 0 4px 12px #00000014;\n}\n\n.dshc_boardTaskHeader {\n align-items: flex-start;\n gap: 8px;\n display: flex;\n}\n\n.dshc_boardTaskTitle {\n overflow-wrap: anywhere;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n flex: 1;\n font-size: 14px;\n font-weight: 600;\n line-height: 1.5;\n display: -webkit-box;\n overflow: hidden;\n}\n\n.dshc_boardTaskHeader .dshc_taskStatus {\n border-radius: 999px;\n padding: 3px 7px;\n font-size: 11px;\n}\n\n.dshc_boardTaskCard .dshc_taskLinks {\n justify-content: flex-end;\n margin-top: auto;\n}\n\n.dshc_boardTabEmpty {\n min-height: 240px;\n color: var(--dsw-alias-label-secondary);\n text-align: center;\n justify-content: center;\n align-items: center;\n padding: 40px 16px;\n display: flex;\n}\n\n@media (width <= 640px) {\n .dshc_boardCardGrid {\n grid-template-columns: minmax(0, 1fr);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .dshc_boardTaskCard {\n transition: none;\n }\n\n .dshc_boardTaskCard:hover {\n transform: none;\n }\n}\n\n.dshc_taskBoardScope {\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n}\n\n.dshc_taskSection + .dshc_taskSection {\n margin-top: 8px;\n}\n\n.dshc_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.dshc_taskOwnerGroup + .dshc_taskOwnerGroup {\n margin-top: 4px;\n}\n\n.dshc_taskOwnerName {\n color: var(--dsw-alias-label-tertiary);\n padding: 0 8px;\n font-size: 12px;\n}\n\n.dshc_taskList {\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.dshc_taskRow {\n border-radius: var(--dsw-radius-sm);\n padding: 6px 8px;\n}\n\n.dshc_taskRow:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_taskLine {\n align-items: center;\n gap: 6px;\n min-width: 0;\n display: flex;\n}\n\n.dshc_taskTitle {\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 13px;\n overflow: hidden;\n}\n\n.dshc_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.dshc_taskLinks {\n gap: 8px;\n display: flex;\n}\n\n.dshc_taskEmpty {\n color: var(--dsw-alias-label-tertiary);\n margin: 0 8px;\n font-size: 12px;\n}\n\n.dshc_taskDone summary {\n cursor: pointer;\n}\n\n.dshc_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.dshc_taskStatus[data-status=\"asked\"], .dshc_taskStatus[data-status=\"escalated\"], .dshc_taskStatus[data-status=\"declined\"], .dshc_taskStatus[data-status=\"rework\"] {\n background: var(--dsw-alias-state-warning-primary, var(--dsw-alias-state-error-primary));\n}\n\n.dshc_taskStatus[data-status=\"done\"] {\n background: var(--dsw-alias-label-tertiary);\n}\n\n.dshc_taskCard {\n margin-top: 4px;\n font-style: normal;\n}\n\n.dshc_taskMembers {\n margin: 4px 0 0;\n padding: 0;\n list-style: none;\n}\n\n.dshc_taskMember {\n align-items: center;\n gap: 8px;\n font-size: 13px;\n display: flex;\n}\n\n.dshc_taskMemberName {\n font-weight: 600;\n}\n\n.dshc_taskSub {\n color: var(--dsw-alias-label-secondary);\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.dshc_focused {\n background: var(--dsw-alias-interactive-bg-hover);\n outline: 2px solid var(--dsw-alias-state-business-primary);\n}\n\n.dshc_attachments {\n flex-wrap: wrap;\n gap: 6px;\n margin: 4px 0;\n display: flex;\n}\n\n.dshc_attachmentImage {\n border-radius: 4px;\n max-width: min(320px, 100%);\n max-height: 240px;\n display: block;\n}\n\n.dshc_attachmentThumb {\n object-fit: cover;\n vertical-align: middle;\n border-radius: 3px;\n width: 32px;\n height: 32px;\n}\n\n.dshc_attachmentFile {\n background: var(--background-secondary, #7f7f7f26);\n color: inherit;\n border-radius: 4px;\n align-items: center;\n gap: 4px;\n padding: 4px 8px;\n text-decoration: none;\n display: inline-flex;\n}\n\n.dshc_attachmentSize {\n opacity: .6;\n font-size: .85em;\n}\n\n.dshc_attachmentAudio {\n flex-direction: column;\n gap: 2px;\n width: min(320px, 100%);\n display: flex;\n}\n\n.dshc_attachmentAudio audio {\n width: 100%;\n}\n\n.dshc_attachmentAudio a {\n color: inherit;\n text-decoration: none;\n}\n\n.dshc_attachmentFile {\n overflow-wrap: anywhere;\n max-width: 100%;\n}\n\n.dshc_chatHead button, .dshc_chatHead input, .dshc_chatHead select, .dshc_chatHead a, .dshc_chatHead [role=\"menu\"] {\n -webkit-app-region: no-drag;\n}\n\n.dshc_text pre, .dshc_text table {\n max-width: 100%;\n overflow-x: auto;\n}\n";document.head.appendChild(s)})();
8
+ (function(){var s=document.createElement('style');s.setAttribute('data-dsh-plugin',"bothouse-dsh");s.textContent=".dshc_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.dshc_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.dshc_rooms, .dshc_members {\n background: var(--dsw-alias-bg-layer-2);\n min-height: 0;\n padding: 0 8px 16px;\n overflow-y: auto;\n}\n\n.dshc_membersHead {\n border-bottom: 1px solid var(--dsw-alias-border-l2);\n flex: none;\n height: 44px;\n margin: 0 0 8px;\n}\n\n.dshc_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.dshc_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.dshc_room:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_room:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color);\n}\n\n.dshc_workspace {\n padding-left: 22px;\n font-size: .95em;\n}\n\n.dshc_roomLabel {\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.dshc_activeCount {\n color: var(--dsw-alias-label-tertiary);\n flex-shrink: 0;\n margin-left: auto;\n font-size: 11px;\n}\n\n.dshc_activeBusy {\n color: var(--dsw-alias-label-primary);\n}\n\n.dshc_roomActive {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_hash {\n color: var(--dsw-alias-label-tertiary);\n font-size: 16px;\n}\n\n.dshc_chat {\n flex-direction: column;\n min-width: 0;\n min-height: 0;\n display: flex;\n}\n\n.dshc_chatHead {\n -webkit-app-region: drag;\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.dshc_log {\n flex: 1;\n min-height: 0;\n padding: 8px 0 12px;\n overflow-y: auto;\n}\n\n.dshc_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.dshc_empty {\n color: var(--dsw-alias-label-tertiary);\n padding: 24px 16px;\n}\n\n.dshc_message {\n grid-template-columns: 36px minmax(0, 1fr);\n gap: 12px;\n padding: 6px 16px;\n display: grid;\n}\n\n.dshc_message:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_system .dshc_text {\n color: var(--dsw-alias-label-tertiary);\n font-style: italic;\n}\n\n.dshc_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.dshc_avatarImage {\n object-fit: cover;\n border-radius: 50%;\n width: 100%;\n height: 100%;\n position: absolute;\n inset: 0;\n}\n\n.dshc_avatarSmall {\n width: 28px;\n height: 28px;\n font-size: 12px;\n}\n\n.dshc_avatarAnchor {\n align-self: start;\n position: relative;\n}\n\n.dshc_avatarButton {\n cursor: pointer;\n background: none;\n border: 0;\n border-radius: 50%;\n padding: 0;\n}\n\n.dshc_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.dshc_avatarMenuTitle {\n padding: 4px 8px;\n font-weight: 700;\n}\n\n.dshc_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.dshc_avatarMenuItem:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_messageBody {\n min-width: 0;\n}\n\n.dshc_messageHead {\n align-items: baseline;\n gap: 6px;\n display: flex;\n}\n\n.dshc_author {\n font-weight: 600;\n}\n\n.dshc_time {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n}\n\n.dshc_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.dshc_edited {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n}\n\n.dshc_ownActions {\n margin-left: auto;\n margin-right: var(--channels-reply-clearance);\n opacity: 0;\n gap: 8px;\n display: inline-flex;\n}\n\n.dshc_message:hover .dshc_ownActions, .dshc_ownActions:focus-within {\n opacity: 1;\n}\n\n@media (hover: none) {\n .dshc_ownActions {\n opacity: 1;\n }\n}\n\n.dshc_editBox {\n flex-direction: column;\n gap: 6px;\n margin-top: 4px;\n display: flex;\n}\n\n.dshc_editInput {\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-height: 60px;\n color: inherit;\n font: inherit;\n resize: vertical;\n padding: 8px;\n}\n\n.dshc_editBar {\n justify-content: flex-end;\n align-items: center;\n gap: 8px;\n display: flex;\n}\n\n.dshc_editBar .dshc_time {\n margin-right: auto;\n}\n\n.dshc_text {\n white-space: normal;\n overflow-wrap: anywhere;\n}\n\n.dshc_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.dshc_card[data-state=\"failed\"] {\n border-left-color: var(--dsw-alias-state-error-primary);\n}\n\n.dshc_card[data-state=\"retrying\"] {\n border-left-color: var(--dsw-alias-state-warning-primary, #faa61a);\n}\n\n.dshc_cardButtons {\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n margin-top: 6px;\n display: flex;\n}\n\n.dshc_cardTitle {\n font-size: 13px;\n font-weight: 600;\n}\n\n.dshc_cardHead {\n justify-content: space-between;\n align-items: center;\n gap: 8px;\n display: flex;\n}\n\n.dshc_cardViews {\n gap: 4px;\n display: flex;\n}\n\n.dshc_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.dshc_cardViewButton[aria-pressed=\"true\"] {\n background: var(--dsw-alias-bg-base);\n font-weight: 600;\n}\n\n.dshc_permActions {\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n margin-top: 6px;\n display: flex;\n}\n\n.dshc_permScope {\n color: var(--dsw-alias-text-secondary);\n flex-basis: 100%;\n font-size: 12px;\n}\n\n.dshc_permStatus {\n margin-top: 6px;\n font-size: 13px;\n font-weight: 600;\n}\n\n.dshc_message {\n --channels-reply-clearance: 36px;\n position: relative;\n}\n\n.dshc_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.dshc_quoteDeleted {\n font-style: italic;\n}\n\n.dshc_quoteAuthor {\n flex: none;\n font-weight: 600;\n}\n\n.dshc_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.dshc_message:hover .dshc_replyButton, .dshc_message:focus-within .dshc_replyButton, .dshc_replyButton:focus-visible {\n opacity: 1;\n}\n\n@media (hover: none) {\n .dshc_replyButton {\n opacity: 1;\n }\n}\n\n.dshc_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.dshc_replyBarText {\n white-space: nowrap;\n text-overflow: ellipsis;\n flex: 1;\n min-width: 0;\n overflow: hidden;\n}\n\n.dshc_replyCancel {\n color: inherit;\n cursor: pointer;\n background: none;\n border: 0;\n flex: none;\n}\n\n.dshc_working {\n min-height: 20px;\n color: var(--dsw-alias-label-tertiary);\n padding: 0 16px;\n font-size: 12px;\n}\n\n.dshc_composer {\n padding: 0 16px 16px;\n position: relative;\n}\n\n.dshc_composerBox {\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: var(--dsw-radius-xl);\n background: var(--dsw-alias-bg-layer-1, var(--dsw-alias-bg-layer-2));\n box-shadow: var(--dsw-shadow-lv1-blur);\n flex-direction: column;\n display: flex;\n overflow: hidden;\n}\n\n.dshc_composerBox:focus-within {\n border-color: var(--dsw-alias-label-tertiary);\n}\n\n.dshc_composerToolbar {\n flex: none;\n align-items: center;\n gap: 4px;\n padding: 4px 10px 10px;\n display: flex;\n}\n\n.dshc_composerBox > .dshc_replyBar, .dshc_composerBox > .dshc_attachments {\n margin: 10px 14px 0;\n}\n\n.dshc_mentionButton {\n border-radius: var(--dsw-radius-full, 50%);\n width: 32px;\n height: 32px;\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n cursor: pointer;\n background: none;\n border: 0;\n flex: none;\n justify-content: center;\n align-items: center;\n padding: 0;\n font-size: 18px;\n display: inline-flex;\n}\n\n.dshc_mentionButton:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_sendButton {\n border-radius: var(--dsw-radius-full, 50%);\n width: 32px;\n height: 32px;\n color: var(--dsw-alias-label-primary-inverted);\n background: var(--dsw-alias-label-primary);\n font: inherit;\n cursor: pointer;\n border: 0;\n flex: none;\n justify-content: center;\n align-items: center;\n margin-left: auto;\n padding: 0;\n display: inline-flex;\n}\n\n.dshc_sendButton:disabled {\n opacity: .45;\n cursor: default;\n}\n\n.dshc_sendButton:focus-visible, .dshc_mentionButton:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color);\n}\n\n.dshc_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.dshc_mentionPopup > [role=\"listbox\"] {\n min-height: 0;\n overflow-y: auto;\n}\n\n.dshc_mentionTitle {\n color: var(--dsw-alias-label-tertiary);\n padding: 4px 8px 6px;\n font-size: 12px;\n font-weight: 600;\n}\n\n.dshc_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.dshc_mentionActive {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_mentionName {\n font-weight: 600;\n}\n\n.dshc_mentionMeta {\n color: var(--dsw-alias-label-tertiary);\n margin-left: auto;\n font-size: 12px;\n}\n\n.dshc_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.dshc_sendError {\n color: var(--dsw-alias-state-error-primary);\n margin-bottom: 6px;\n font-size: 12px;\n}\n\n.dshc_input {\n box-sizing: border-box;\n width: 100%;\n min-height: 52px;\n max-height: 40vh;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n resize: none;\n field-sizing: content;\n background: none;\n border: 0;\n border-radius: 0;\n flex: 1;\n min-width: 0;\n padding: 14px 16px 8px;\n line-height: 1.5;\n}\n\n.dshc_input::placeholder {\n white-space: nowrap;\n text-overflow: ellipsis;\n overflow: hidden;\n}\n\n@media (width <= 600px) {\n .dshc_composer {\n padding: 0 8px 8px;\n }\n\n .dshc_composerToolbar {\n padding: 4px 8px 8px;\n }\n\n .dshc_input {\n padding: 12px 14px 8px;\n }\n}\n\n.dshc_input:focus-visible {\n outline: none;\n}\n\n.dshc_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.dshc_member:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_member:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color);\n}\n\n.dshc_offline {\n opacity: .5;\n}\n\n.dshc_memberAvatar {\n position: relative;\n}\n\n.dshc_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.dshc_offline .dshc_presence {\n background: var(--dsw-alias-label-tertiary);\n}\n\n.dshc_memberText {\n flex-direction: column;\n min-width: 0;\n display: flex;\n}\n\n.dshc_memberName {\n align-items: center;\n gap: 6px;\n display: flex;\n}\n\n.dshc_memberStatus {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n}\n\n@media (width <= 900px) {\n .dshc_page {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .dshc_rooms, .dshc_members {\n display: none;\n }\n}\n\n.dshc_groupHead {\n justify-content: space-between;\n align-items: center;\n padding-right: 4px;\n display: flex;\n}\n\n.dshc_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.dshc_iconButton:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_roomInput, .dshc_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.dshc_roomInput {\n margin: 2px 0 6px;\n}\n\n.dshc_roomInput:focus-visible, .dshc_fieldInput:focus-visible, .dshc_iconButton:focus-visible, .dshc_linkButton:focus-visible, .dshc_addMemberButton:focus-visible, .dshc_primaryButton:focus-visible, .dshc_secondaryButton:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color);\n}\n\n.dshc_addMemberButton {\n box-sizing: border-box;\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.dshc_addMemberButton:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_settingsDivider {\n border: 0;\n border-top: 1px solid var(--dsw-alias-border-l1);\n width: 100%;\n margin: 8px 0 4px;\n}\n\n.dshc_memberListDivider {\n border: 0;\n border-top: 1px solid var(--dsw-alias-border-l1);\n width: calc(100% - 16px);\n margin: 12px 8px 2px;\n}\n\n.dshc_settingsButton {\n text-align: left;\n border-style: solid;\n border-color: #0000;\n align-items: center;\n gap: 8px;\n display: flex;\n}\n\n.dshc_settingsButton svg {\n flex: none;\n}\n\n.dshc_memberRow {\n border-radius: var(--dsw-radius-sm);\n align-items: center;\n display: flex;\n position: relative;\n}\n\n.dshc_memberRow:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_memberRow .dshc_member:hover {\n background: none;\n}\n\n.dshc_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.dshc_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.dshc_memberRow:hover .dshc_memberActions, .dshc_memberRow:focus-within .dshc_memberActions {\n opacity: 1;\n}\n\n.dshc_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.dshc_linkButton:hover {\n color: var(--dsw-alias-label-primary);\n text-decoration: underline;\n}\n\n.dshc_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.dshc_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.dshc_settingsDialog {\n box-sizing: border-box;\n height: min(70vh, 100dvh - 32px);\n max-height: calc(100dvh - 32px);\n overflow: hidden;\n}\n\n.dshc_settingsBody {\n overscroll-behavior: contain;\n flex: auto;\n min-height: 0;\n padding: 2px;\n overflow-y: auto;\n}\n\n.dshc_settingsDialog > :not(.dshc_settingsBody) {\n flex-shrink: 0;\n}\n\n.dshc_settingsFields {\n flex-direction: column;\n gap: 12px;\n display: flex;\n}\n\n.dshc_settingsFeedback {\n overflow-wrap: anywhere;\n max-height: 20%;\n overflow-y: auto;\n}\n\n.dshc_settingsDialog .dshc_dialogActions {\n justify-content: space-between;\n}\n\n@media (width <= 599px) {\n .dshc_settingsDialog {\n height: calc(100dvh - 32px);\n padding: 16px;\n }\n}\n\n.dshc_avatarCropStage {\n background: #000000a6;\n padding: 20px;\n}\n\n.dshc_avatarCropFrame {\n aspect-ratio: 1;\n touch-action: none;\n cursor: grab;\n outline: 2px solid #fff;\n width: min(256px, 100%);\n margin: auto;\n position: relative;\n overflow: hidden;\n}\n\n.dshc_avatarCropFrame:active {\n cursor: grabbing;\n}\n\n.dshc_avatarCropFrame img {\n pointer-events: none;\n user-select: none;\n max-width: none;\n position: absolute;\n}\n\n.dshc_dialogTitle {\n margin: 0;\n font-size: 16px;\n}\n\n.dshc_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.dshc_field textarea {\n resize: vertical;\n}\n\n.dshc_choiceRow {\n flex-wrap: wrap;\n gap: 12px;\n display: flex;\n}\n\n.dshc_choiceColumn {\n flex-direction: column;\n gap: 12px;\n display: flex;\n}\n\n.dshc_choiceColumn .dshc_choice {\n grid-template-columns: max-content minmax(0, 1fr);\n align-items: start;\n display: grid;\n}\n\n.dshc_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.dshc_dialogActions {\n justify-content: flex-end;\n gap: 8px;\n display: flex;\n}\n\n.dshc_primaryButton, .dshc_secondaryButton {\n border-radius: var(--dsw-radius-sm);\n font: inherit;\n cursor: pointer;\n padding: 7px 14px;\n}\n\n.dshc_primaryButton {\n color: #fff;\n background: var(--dsw-alias-state-business-primary);\n border: 0;\n}\n\n.dshc_primaryButton:disabled {\n cursor: not-allowed;\n opacity: .5;\n}\n\n.dshc_secondaryButton {\n border: 1px solid var(--dsw-alias-border-l2);\n color: var(--dsw-alias-label-primary);\n background: none;\n}\n\n.dshc_memberFailed .dshc_memberStatus {\n color: var(--dsw-alias-state-error-primary, #ed4245);\n}\n\n.dshc_memberFailed .dshc_presence {\n background: #ed4245;\n}\n\n.dshc_mobileButton, .dshc_drawerBackdrop {\n display: none;\n}\n\n.dshc_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.dshc_panelToggle:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_panelToggle:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color);\n}\n\n.dshc_panelToggleEnd {\n margin-left: auto;\n}\n\n.dshc_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.dshc_capabilityItem:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_capabilityHelp {\n color: var(--dsw-alias-label-secondary);\n white-space: normal;\n margin: 6px;\n font-size: 12px;\n}\n\n.dshc_capabilityItem:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color);\n}\n\n.dshc_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 .dshc_page.dshc_roomsHidden {\n grid-template-columns: minmax(0, 1fr) 240px;\n }\n\n .dshc_page.dshc_membersHidden {\n grid-template-columns: 220px minmax(0, 1fr);\n }\n\n .dshc_page.dshc_roomsHidden.dshc_membersHidden {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .dshc_roomsHidden > .dshc_rooms, .dshc_membersHidden > .dshc_members {\n display: none;\n }\n}\n\n@media (width <= 900px) {\n .dshc_page {\n position: relative;\n overflow: hidden;\n }\n\n .dshc_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 .dshc_mobileButton:last-child {\n margin-left: auto;\n }\n\n .dshc_panelToggle {\n display: none;\n }\n\n .dshc_drawerBackdrop {\n z-index: 4;\n background: #00000073;\n border: 0;\n display: block;\n position: absolute;\n inset: 0;\n }\n\n .dshc_rooms.dshc_drawerOpen, .dshc_members.dshc_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 .dshc_rooms.dshc_drawerOpen {\n left: 0;\n }\n\n .dshc_members.dshc_drawerOpen {\n right: 0;\n }\n\n .dshc_memberActions {\n opacity: 1;\n }\n\n .dshc_messageHead {\n flex-wrap: wrap;\n }\n}\n\n.dshc_avatarPicker {\n flex-wrap: wrap;\n gap: 8px;\n display: flex;\n}\n\n.dshc_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.dshc_avatarOption[aria-checked=\"true\"] {\n border-color: var(--dsw-alias-accent, #5865f2);\n}\n\n.dshc_taskBoardTrigger {\n box-sizing: border-box;\n border-radius: var(--dsw-radius-sm);\n width: 100%;\n height: 34px;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n text-align: left;\n cursor: pointer;\n background: none;\n border: 0;\n flex: none;\n align-items: center;\n gap: 8px;\n margin: 4px 0;\n padding: 7px 8px;\n font-size: 12px;\n display: flex;\n}\n\n.dshc_taskBoardTrigger:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_taskBoardTrigger:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color);\n}\n\n.dshc_taskBoardSummary {\n color: var(--dsw-alias-label-secondary);\n text-align: right;\n text-overflow: ellipsis;\n white-space: nowrap;\n flex: 1;\n overflow: hidden;\n}\n\n.dshc_taskBoardDialog {\n width: min(920px, 100%);\n height: min(70vh, 100dvh - 32px);\n overflow: hidden;\n}\n\n.dshc_taskBoardBody {\n overscroll-behavior: contain;\n flex: auto;\n min-height: 0;\n overflow-y: auto;\n}\n\n.dshc_taskBoardDialog > :not(.dshc_taskBoardBody) {\n flex-shrink: 0;\n}\n\n.dshc_boardTabs {\n border-bottom: 1px solid var(--dsw-alias-border-l2);\n gap: 4px;\n padding-bottom: 8px;\n display: flex;\n overflow-x: auto;\n}\n\n.dshc_boardTabs button {\n border-radius: var(--dsw-radius-sm);\n color: var(--dsw-alias-label-secondary);\n font: inherit;\n cursor: pointer;\n background: none;\n border: 0;\n flex: none;\n padding: 7px 10px;\n font-size: 13px;\n}\n\n.dshc_boardTabs button:hover, .dshc_boardTabs button[aria-selected=\"true\"] {\n color: var(--dsw-alias-state-business-primary);\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_boardTabs button:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color);\n outline-offset: -2px;\n}\n\n.dshc_boardTabCount {\n font-variant-numeric: tabular-nums;\n margin-left: 4px;\n}\n\n.dshc_boardCardGrid {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 12px;\n margin: 6px 0 16px;\n padding: 2px;\n list-style: none;\n display: grid;\n}\n\n.dshc_boardTaskCard {\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-base);\n border-radius: 12px;\n flex-direction: column;\n gap: 10px;\n min-width: 0;\n padding: 14px;\n transition: transform .14s, box-shadow .14s;\n display: flex;\n}\n\n.dshc_boardTaskCard[data-working=\"true\"] {\n border-color: var(--dsw-alias-state-business-primary);\n}\n\n.dshc_boardTaskCard[data-status=\"done\"] {\n filter: saturate(.25);\n color: var(--dsw-alias-label-secondary);\n}\n\n.dshc_boardTaskCard:hover {\n transform: translateY(-2px);\n box-shadow: 0 4px 12px #00000014;\n}\n\n.dshc_boardTaskHeader {\n align-items: flex-start;\n gap: 8px;\n display: flex;\n}\n\n.dshc_boardTaskTitle {\n overflow-wrap: anywhere;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n flex: 1;\n font-size: 14px;\n font-weight: 600;\n line-height: 1.5;\n display: -webkit-box;\n overflow: hidden;\n}\n\n.dshc_boardTaskHeader .dshc_taskStatus {\n border-radius: 999px;\n padding: 3px 7px;\n font-size: 11px;\n}\n\n.dshc_boardTaskCard .dshc_taskLinks {\n justify-content: flex-end;\n margin-top: auto;\n}\n\n.dshc_boardTabEmpty {\n min-height: 240px;\n color: var(--dsw-alias-label-secondary);\n text-align: center;\n justify-content: center;\n align-items: center;\n padding: 40px 16px;\n display: flex;\n}\n\n@media (width <= 640px) {\n .dshc_boardCardGrid {\n grid-template-columns: minmax(0, 1fr);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .dshc_boardTaskCard {\n transition: none;\n }\n\n .dshc_boardTaskCard:hover {\n transform: none;\n }\n}\n\n.dshc_taskBoardScope {\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n}\n\n.dshc_taskSection + .dshc_taskSection {\n margin-top: 8px;\n}\n\n.dshc_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.dshc_taskOwnerGroup + .dshc_taskOwnerGroup {\n margin-top: 4px;\n}\n\n.dshc_taskOwnerName {\n color: var(--dsw-alias-label-tertiary);\n padding: 0 8px;\n font-size: 12px;\n}\n\n.dshc_taskList {\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.dshc_taskRow {\n border-radius: var(--dsw-radius-sm);\n padding: 6px 8px;\n}\n\n.dshc_taskRow:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dshc_taskLine {\n align-items: center;\n gap: 6px;\n min-width: 0;\n display: flex;\n}\n\n.dshc_taskTitle {\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 13px;\n overflow: hidden;\n}\n\n.dshc_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.dshc_taskLinks {\n gap: 8px;\n display: flex;\n}\n\n.dshc_taskEmpty {\n color: var(--dsw-alias-label-tertiary);\n margin: 0 8px;\n font-size: 12px;\n}\n\n.dshc_taskDone summary {\n cursor: pointer;\n}\n\n.dshc_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.dshc_taskStatus[data-status=\"asked\"], .dshc_taskStatus[data-status=\"escalated\"], .dshc_taskStatus[data-status=\"declined\"], .dshc_taskStatus[data-status=\"rework\"] {\n background: var(--dsw-alias-state-warning-primary, var(--dsw-alias-state-error-primary));\n}\n\n.dshc_taskStatus[data-status=\"done\"] {\n background: var(--dsw-alias-label-tertiary);\n}\n\n.dshc_taskCard {\n margin-top: 4px;\n font-style: normal;\n}\n\n.dshc_taskMembers {\n margin: 4px 0 0;\n padding: 0;\n list-style: none;\n}\n\n.dshc_taskMember {\n align-items: center;\n gap: 8px;\n font-size: 13px;\n display: flex;\n}\n\n.dshc_taskMemberName {\n font-weight: 600;\n}\n\n.dshc_taskSub {\n color: var(--dsw-alias-label-secondary);\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.dshc_focused {\n background: var(--dsw-alias-interactive-bg-hover);\n outline: 2px solid var(--dsw-alias-state-business-primary);\n}\n\n.dshc_attachments {\n flex-wrap: wrap;\n gap: 6px;\n margin: 4px 0;\n display: flex;\n}\n\n.dshc_attachmentImage {\n border-radius: 4px;\n max-width: min(320px, 100%);\n max-height: 240px;\n display: block;\n}\n\n.dshc_imageAttachment {\n align-content: start;\n max-width: 100%;\n display: grid;\n}\n\n.dshc_imagePreviewTrigger {\n cursor: zoom-in;\n background: none;\n border: 0;\n border-radius: 4px;\n grid-area: 1 / 1;\n padding: 0;\n}\n\n.dshc_imagePreviewTrigger:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 2px;\n}\n\n.dshc_imagePreviewDialog {\n box-sizing: border-box;\n width: fit-content;\n max-width: 90vw;\n max-height: 90dvh;\n overflow: auto;\n}\n\n.dshc_imagePreviewHeader {\n flex-shrink: 0;\n justify-content: space-between;\n align-items: center;\n gap: 12px;\n display: flex;\n}\n\n.dshc_imagePreviewHeader span {\n overflow-wrap: anywhere;\n min-width: 0;\n}\n\n.dshc_imagePreviewHeader button {\n flex-shrink: 0;\n}\n\n.dshc_imagePreviewImage {\n object-fit: contain;\n align-self: center;\n max-width: 100%;\n min-height: 0;\n max-height: 65dvh;\n display: block;\n}\n\n.dshc_imagePreviewActions {\n flex-wrap: wrap;\n flex-shrink: 0;\n gap: 8px;\n display: flex;\n}\n\n.dshc_imagePreviewActions a {\n text-decoration: none;\n}\n\n.dshc_imagePreviewActions button:disabled {\n cursor: wait;\n opacity: .5;\n}\n\n.dshc_attachmentActions {\n border-radius: var(--dsw-radius-sm);\n background: var(--dsw-alias-bg-base);\n opacity: 0;\n pointer-events: none;\n flex-wrap: wrap;\n grid-area: 1 / 1;\n place-self: start end;\n gap: 4px;\n margin: 4px;\n padding: 4px;\n display: flex;\n}\n\n.dshc_attachmentActions a {\n text-decoration: none;\n}\n\n.dshc_attachmentActions button:disabled {\n cursor: wait;\n opacity: .5;\n}\n\n.dshc_imageAttachment:hover .dshc_attachmentActions, .dshc_imageAttachment:focus-within .dshc_attachmentActions, .dshc_imageAttachment[data-copy-state=\"error\"] .dshc_attachmentActions, .dshc_imageAttachment[data-copy-state=\"copying\"] .dshc_attachmentActions, .dshc_imageAttachment[data-copy-state=\"copied\"] .dshc_attachmentActions {\n opacity: 1;\n pointer-events: auto;\n}\n\n@media (hover: none), (pointer: coarse), (width <= 768px) {\n .dshc_attachmentActions {\n opacity: 1;\n pointer-events: auto;\n }\n}\n\n.dshc_attachmentThumb {\n object-fit: cover;\n vertical-align: middle;\n border-radius: 3px;\n width: 32px;\n height: 32px;\n}\n\n.dshc_attachmentFile {\n background: var(--background-secondary, #7f7f7f26);\n color: inherit;\n border-radius: 4px;\n align-items: center;\n gap: 4px;\n padding: 4px 8px;\n text-decoration: none;\n display: inline-flex;\n}\n\n.dshc_attachmentSize {\n opacity: .6;\n font-size: .85em;\n}\n\n.dshc_attachmentAudio {\n flex-direction: column;\n gap: 2px;\n width: min(320px, 100%);\n display: flex;\n}\n\n.dshc_attachmentAudio audio {\n width: 100%;\n}\n\n.dshc_attachmentAudio a {\n color: inherit;\n text-decoration: none;\n}\n\n.dshc_attachmentFile {\n overflow-wrap: anywhere;\n max-width: 100%;\n}\n\n.dshc_chatHead button, .dshc_chatHead input, .dshc_chatHead select, .dshc_chatHead a, .dshc_chatHead [role=\"menu\"] {\n -webkit-app-region: no-drag;\n}\n\n.dshc_text pre, .dshc_text table {\n max-width: 100%;\n overflow-x: auto;\n}\n";document.head.appendChild(s)})();
9
9
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
10
10
  // \0rolldown/runtime.js
11
11
  var __defProp = Object.defineProperty;
@@ -252,7 +252,8 @@ function createChannelSource(transport) {
252
252
  ...r,
253
253
  lead_id: event.lead_id,
254
254
  effective_lead_id: event.effective_lead_id,
255
- workspace_path: event.workspace_path
255
+ workspace_path: event.workspace_path,
256
+ max_relay: event.max_relay ?? r.max_relay
256
257
  } : r)
257
258
  };
258
259
  case "lead": return {
@@ -631,6 +632,15 @@ function createChannelSource(transport) {
631
632
  ...result
632
633
  } : r) });
633
634
  },
635
+ async setMaxRelay(roomId, maxRelay) {
636
+ const result = await request(`collab/api/rooms/${roomId}/settings`, json("PATCH", { max_relay: maxRelay }));
637
+ const chat = snapshot.rooms.find((r) => r.id === roomId)?.parent_id ?? roomId;
638
+ if (lifetime !== void 0) onEvent({
639
+ type: "room-settings",
640
+ room_id: chat,
641
+ ...result
642
+ });
643
+ },
634
644
  async setLead(id, roomId = snapshot.roomId) {
635
645
  if (roomId === void 0) throw new Error("No channel is open");
636
646
  const result = await request(`collab/api/rooms/${roomId}/settings`, json("PATCH", { lead_id: id }));
@@ -16413,6 +16423,7 @@ var ChannelsPage_module_default = {
16413
16423
  "activeBusy": "dshc_activeBusy",
16414
16424
  "activeCount": "dshc_activeCount",
16415
16425
  "addMemberButton": "dshc_addMemberButton",
16426
+ "attachmentActions": "dshc_attachmentActions",
16416
16427
  "attachmentAudio": "dshc_attachmentAudio",
16417
16428
  "attachmentFile": "dshc_attachmentFile",
16418
16429
  "attachmentImage": "dshc_attachmentImage",
@@ -16475,6 +16486,12 @@ var ChannelsPage_module_default = {
16475
16486
  "guide": "dshc_guide",
16476
16487
  "hash": "dshc_hash",
16477
16488
  "iconButton": "dshc_iconButton",
16489
+ "imageAttachment": "dshc_imageAttachment",
16490
+ "imagePreviewActions": "dshc_imagePreviewActions",
16491
+ "imagePreviewDialog": "dshc_imagePreviewDialog",
16492
+ "imagePreviewHeader": "dshc_imagePreviewHeader",
16493
+ "imagePreviewImage": "dshc_imagePreviewImage",
16494
+ "imagePreviewTrigger": "dshc_imagePreviewTrigger",
16478
16495
  "input": "dshc_input",
16479
16496
  "linkButton": "dshc_linkButton",
16480
16497
  "log": "dshc_log",
@@ -16570,22 +16587,163 @@ var ChannelsPage_module_default = {
16570
16587
  const isImage = (mime) => /^image\/(png|jpeg|gif|webp)$/.test(mime);
16571
16588
  const isAudio = (mime) => mime.startsWith("audio/");
16572
16589
  const size = (bytes) => bytes < 1024 ? `${bytes} B` : bytes < 1048576 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1024 / 1024).toFixed(1)} MB`;
16590
+ function ImageAttachment({ attachment: a, t }) {
16591
+ const [opened, setOpened] = (0, react.useState)(false);
16592
+ const trigger = (0, react.useRef)(null);
16593
+ const dialog = (0, react.useRef)(null);
16594
+ const closeButton = (0, react.useRef)(null);
16595
+ (0, react.useEffect)(() => {
16596
+ if (!opened) return;
16597
+ const origin = trigger.current;
16598
+ closeButton.current?.focus();
16599
+ const containFocus = (event) => {
16600
+ if (!dialog.current?.contains(event.target)) closeButton.current?.focus();
16601
+ };
16602
+ document.addEventListener("focusin", containFocus);
16603
+ return () => {
16604
+ document.removeEventListener("focusin", containFocus);
16605
+ origin?.focus();
16606
+ };
16607
+ }, [opened]);
16608
+ const [copyState, setCopyState] = (0, react.useState)("idle");
16609
+ (0, react.useEffect)(() => {
16610
+ if (copyState !== "copied") return;
16611
+ const timer = setTimeout(() => setCopyState("idle"), 2e3);
16612
+ return () => clearTimeout(timer);
16613
+ }, [copyState]);
16614
+ const copy = async () => {
16615
+ if (copyState === "copying") return;
16616
+ setCopyState("copying");
16617
+ try {
16618
+ if (typeof navigator.clipboard?.write !== "function" || typeof ClipboardItem === "undefined") {
16619
+ setCopyState("error");
16620
+ return;
16621
+ }
16622
+ const response = await fetch(attachmentUrl(a.id));
16623
+ if (!response.ok) throw new Error("Image download failed");
16624
+ const blob = await response.blob();
16625
+ await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
16626
+ setCopyState("copied");
16627
+ } catch {
16628
+ setCopyState("error");
16629
+ }
16630
+ };
16631
+ const copyLabel = t(copyState === "copied" ? "attachmentCopied" : copyState === "copying" ? "attachmentCopying" : "attachmentCopy");
16632
+ const actions = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
16633
+ className: copyState === "error" ? ChannelsPage_module_default.primaryButton : ChannelsPage_module_default.secondaryButton,
16634
+ href: attachmentUrl(a.id),
16635
+ download: a.name,
16636
+ target: "_blank",
16637
+ rel: "noreferrer",
16638
+ "aria-label": t("attachmentDownload"),
16639
+ children: t("attachmentDownload")
16640
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
16641
+ type: "button",
16642
+ className: ChannelsPage_module_default.secondaryButton,
16643
+ onClick: () => {
16644
+ copy();
16645
+ },
16646
+ disabled: copyState === "copying",
16647
+ "aria-label": copyLabel,
16648
+ "aria-live": "polite",
16649
+ children: copyLabel
16650
+ })] });
16651
+ const error = copyState === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
16652
+ className: ChannelsPage_module_default.sendError,
16653
+ role: "alert",
16654
+ children: t("attachmentCopyError")
16655
+ });
16656
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
16657
+ className: ChannelsPage_module_default.imageAttachment,
16658
+ "data-copy-state": copyState,
16659
+ children: [
16660
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
16661
+ ref: trigger,
16662
+ type: "button",
16663
+ className: ChannelsPage_module_default.imagePreviewTrigger,
16664
+ title: a.name,
16665
+ "aria-haspopup": "dialog",
16666
+ "aria-expanded": opened,
16667
+ onClick: () => setOpened(true),
16668
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
16669
+ className: ChannelsPage_module_default.attachmentImage,
16670
+ src: attachmentUrl(a.id),
16671
+ alt: a.name,
16672
+ loading: "lazy"
16673
+ })
16674
+ }),
16675
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
16676
+ className: ChannelsPage_module_default.attachmentActions,
16677
+ children: actions
16678
+ }),
16679
+ !opened && error,
16680
+ opened && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
16681
+ className: ChannelsPage_module_default.dialogBackdrop,
16682
+ onClick: (event) => {
16683
+ if (event.target === event.currentTarget) setOpened(false);
16684
+ },
16685
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
16686
+ ref: dialog,
16687
+ className: `${ChannelsPage_module_default.dialog} ${ChannelsPage_module_default.imagePreviewDialog}`,
16688
+ role: "dialog",
16689
+ "aria-modal": "true",
16690
+ "aria-label": a.name,
16691
+ tabIndex: -1,
16692
+ onKeyDown: (event) => {
16693
+ if (event.key === "Escape") {
16694
+ event.preventDefault();
16695
+ event.stopPropagation();
16696
+ setOpened(false);
16697
+ }
16698
+ if (event.key === "Tab") {
16699
+ const controls = Array.from(event.currentTarget.querySelectorAll("button:not(:disabled), a[href]"));
16700
+ const first = controls[0];
16701
+ const last = controls[controls.length - 1];
16702
+ if (event.shiftKey && (document.activeElement === first || document.activeElement === dialog.current)) {
16703
+ event.preventDefault();
16704
+ last?.focus();
16705
+ } else if (!event.shiftKey && (document.activeElement === last || document.activeElement === dialog.current)) {
16706
+ event.preventDefault();
16707
+ first?.focus();
16708
+ }
16709
+ }
16710
+ },
16711
+ children: [
16712
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
16713
+ className: ChannelsPage_module_default.imagePreviewHeader,
16714
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: a.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
16715
+ ref: closeButton,
16716
+ type: "button",
16717
+ className: ChannelsPage_module_default.secondaryButton,
16718
+ "aria-label": t("close"),
16719
+ onClick: () => setOpened(false),
16720
+ children: "×"
16721
+ })]
16722
+ }),
16723
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
16724
+ className: ChannelsPage_module_default.imagePreviewImage,
16725
+ src: attachmentUrl(a.id),
16726
+ alt: a.name
16727
+ }),
16728
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
16729
+ className: ChannelsPage_module_default.imagePreviewActions,
16730
+ children: actions
16731
+ }),
16732
+ error
16733
+ ]
16734
+ })
16735
+ }), document.body)
16736
+ ]
16737
+ });
16738
+ }
16573
16739
  /** Thumbnails for images, a download card for other files. */
16574
- function AttachmentList({ attachments }) {
16740
+ function AttachmentList({ attachments, t }) {
16575
16741
  if (attachments === void 0 || attachments.length === 0) return null;
16576
16742
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
16577
16743
  className: ChannelsPage_module_default.attachments,
16578
- children: attachments.map((a) => isImage(a.mime) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
16579
- href: attachmentUrl(a.id),
16580
- target: "_blank",
16581
- rel: "noreferrer",
16582
- title: a.name,
16583
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
16584
- className: ChannelsPage_module_default.attachmentImage,
16585
- src: attachmentUrl(a.id),
16586
- alt: a.name,
16587
- loading: "lazy"
16588
- })
16744
+ children: attachments.map((a) => isImage(a.mime) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageAttachment, {
16745
+ attachment: a,
16746
+ t
16589
16747
  }, a.id) : isAudio(a.mime) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
16590
16748
  className: ChannelsPage_module_default.attachmentAudio,
16591
16749
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("audio", {
@@ -18124,6 +18282,8 @@ function MemberDialog({ member, members = NO_MEMBERS, listModels, listClis, list
18124
18282
  });
18125
18283
  }
18126
18284
  // src/client/ChannelSettingsDialog.tsx
18285
+ const RELAY_CAP_MIN = 6;
18286
+ const RELAY_CAP_MAX = 20;
18127
18287
  const tabs = [
18128
18288
  "basic",
18129
18289
  "dispatch",
@@ -18135,15 +18295,17 @@ const tabLabels = {
18135
18295
  workspace: "settingsWorkspace"
18136
18296
  };
18137
18297
  /** Drafts and save outcomes live for the whole dialog, independently of the selected tab. */
18138
- function ChannelSettingsDialog({ room, members, effectiveLead, renameRoom, setDispatchMode, setLead, setWorkspacePath, renewRoom, t, onClose }) {
18298
+ function ChannelSettingsDialog({ room, members, effectiveLead, renameRoom, setDispatchMode, setMaxRelay, setLead, setWorkspacePath, renewRoom, t, onClose }) {
18139
18299
  const [tab, setTab] = (0, react.useState)("basic");
18140
18300
  const [name, setName] = (0, react.useState)(room.name);
18141
18301
  const [mode, setMode] = (0, react.useState)(room.dispatch_mode ?? "mixed");
18302
+ const [relay, setRelay] = (0, react.useState)(String(room.max_relay ?? RELAY_CAP_MIN));
18142
18303
  const [lead, setLeadDraft] = (0, react.useState)(effectiveLead);
18143
18304
  const [directory, setDirectory] = (0, react.useState)(room.workspace_path ?? "");
18144
18305
  const [saved, setSaved] = (0, react.useState)({
18145
18306
  name: room.name,
18146
18307
  mode: room.dispatch_mode ?? "mixed",
18308
+ relay: String(room.max_relay ?? RELAY_CAP_MIN),
18147
18309
  lead: effectiveLead,
18148
18310
  directory: room.workspace_path ?? ""
18149
18311
  });
@@ -18156,9 +18318,10 @@ function ChannelSettingsDialog({ room, members, effectiveLead, renameRoom, setDi
18156
18318
  (0, react.useEffect)(() => {
18157
18319
  dialogRef.current?.focus();
18158
18320
  }, []);
18321
+ const relayValid = /^\d+$/.test(relay) && Number(relay) >= RELAY_CAP_MIN && Number(relay) <= RELAY_CAP_MAX;
18159
18322
  const dirty = {
18160
18323
  basic: room.kind !== "workspace" && name !== saved.name,
18161
- dispatch: mode !== saved.mode || lead !== saved.lead,
18324
+ dispatch: mode !== saved.mode || relay !== saved.relay || lead !== saved.lead,
18162
18325
  workspace: directory !== saved.directory
18163
18326
  };
18164
18327
  const close = () => {
@@ -18179,6 +18342,7 @@ function ChannelSettingsDialog({ room, members, effectiveLead, renameRoom, setDi
18179
18342
  const save = async (target) => {
18180
18343
  if (saving[target] || !dirty[target]) return;
18181
18344
  if (target === "basic" && (!name || name.length > 32 || /\s/.test(name))) return;
18345
+ if (target === "dispatch" && !relayValid) return;
18182
18346
  if (target === "workspace" && (!directory || renewing || renewPending)) return;
18183
18347
  setSaving((previous) => ({
18184
18348
  ...previous,
@@ -18207,6 +18371,13 @@ function ChannelSettingsDialog({ room, members, effectiveLead, renameRoom, setDi
18207
18371
  lead
18208
18372
  }));
18209
18373
  }
18374
+ if (relay !== saved.relay) {
18375
+ await setMaxRelay(room.id, Number(relay));
18376
+ setSaved((previous) => ({
18377
+ ...previous,
18378
+ relay
18379
+ }));
18380
+ }
18210
18381
  } else {
18211
18382
  await setWorkspacePath(room.id, directory);
18212
18383
  setSaved((previous) => ({
@@ -18325,51 +18496,75 @@ function ChannelSettingsDialog({ room, members, effectiveLead, renameRoom, setDi
18325
18496
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(room.kind === "workspace" ? "workspaceNameFixed" : "channelNameHint") })
18326
18497
  ]
18327
18498
  }) }),
18328
- tab === "dispatch" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
18329
- className: ChannelsPage_module_default.field,
18330
- role: "radiogroup",
18331
- "aria-label": t("dispatchMode"),
18332
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("dispatchMode") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
18333
- className: ChannelsPage_module_default.choiceColumn,
18499
+ tab === "dispatch" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
18500
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
18501
+ className: ChannelsPage_module_default.field,
18502
+ role: "radiogroup",
18503
+ "aria-label": t("dispatchMode"),
18504
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("dispatchMode") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
18505
+ className: ChannelsPage_module_default.choiceColumn,
18506
+ children: [
18507
+ "solo",
18508
+ "team",
18509
+ "mixed"
18510
+ ].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
18511
+ className: ChannelsPage_module_default.choice,
18512
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
18513
+ type: "radio",
18514
+ name: "settings-dispatch-mode",
18515
+ disabled: saving.dispatch,
18516
+ checked: mode === value,
18517
+ onChange: () => {
18518
+ setMode(value);
18519
+ clearFeedback("dispatch");
18520
+ }
18521
+ }), t(value === "solo" ? "dispatchSolo" : value === "team" ? "dispatchTeam" : "dispatchMixed")]
18522
+ }, value))
18523
+ })]
18524
+ }),
18525
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
18526
+ className: ChannelsPage_module_default.field,
18527
+ children: [t("channelLead"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
18528
+ className: ChannelsPage_module_default.fieldInput,
18529
+ "aria-label": t("channelLead"),
18530
+ value: lead,
18531
+ disabled: saving.dispatch,
18532
+ onChange: (event) => {
18533
+ setLeadDraft(event.target.value);
18534
+ clearFeedback("dispatch");
18535
+ },
18536
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
18537
+ value: "",
18538
+ disabled: true,
18539
+ children: t("leadNone")
18540
+ }), members.filter((member) => member.kind !== "human").map((member) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
18541
+ value: member.id,
18542
+ children: member.name
18543
+ }, member.id))]
18544
+ })]
18545
+ }),
18546
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
18547
+ className: ChannelsPage_module_default.field,
18334
18548
  children: [
18335
- "solo",
18336
- "team",
18337
- "mixed"
18338
- ].map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
18339
- className: ChannelsPage_module_default.choice,
18340
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
18341
- type: "radio",
18342
- name: "settings-dispatch-mode",
18549
+ t("settingsRelay"),
18550
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
18551
+ className: ChannelsPage_module_default.fieldInput,
18552
+ type: "number",
18553
+ min: RELAY_CAP_MIN,
18554
+ max: RELAY_CAP_MAX,
18555
+ step: 1,
18556
+ "aria-label": t("settingsRelay"),
18557
+ value: relay,
18343
18558
  disabled: saving.dispatch,
18344
- checked: mode === value,
18345
- onChange: () => {
18346
- setMode(value);
18559
+ onChange: (event) => {
18560
+ setRelay(event.target.value);
18347
18561
  clearFeedback("dispatch");
18348
18562
  }
18349
- }), t(value === "solo" ? "dispatchSolo" : value === "team" ? "dispatchTeam" : "dispatchMixed")]
18350
- }, value))
18351
- })]
18352
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
18353
- className: ChannelsPage_module_default.field,
18354
- children: [t("channelLead"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
18355
- className: ChannelsPage_module_default.fieldInput,
18356
- "aria-label": t("channelLead"),
18357
- value: lead,
18358
- disabled: saving.dispatch,
18359
- onChange: (event) => {
18360
- setLeadDraft(event.target.value);
18361
- clearFeedback("dispatch");
18362
- },
18363
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
18364
- value: "",
18365
- disabled: true,
18366
- children: t("leadNone")
18367
- }), members.filter((member) => member.kind !== "human").map((member) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
18368
- value: member.id,
18369
- children: member.name
18370
- }, member.id))]
18371
- })]
18372
- })] }),
18563
+ }),
18564
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("settingsRelayHint") })
18565
+ ]
18566
+ })
18567
+ ] }),
18373
18568
  tab === "workspace" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
18374
18569
  className: ChannelsPage_module_default.field,
18375
18570
  children: [
@@ -18456,7 +18651,7 @@ function ChannelSettingsDialog({ room, members, effectiveLead, renameRoom, setDi
18456
18651
  form: "channel-settings-form",
18457
18652
  "aria-busy": Boolean(saving[tab]),
18458
18653
  className: ChannelsPage_module_default.primaryButton,
18459
- disabled: saving[tab] || !dirty[tab] || tab === "basic" && (!name || name.length > 32 || /\s/.test(name)) || tab === "workspace" && (!directory || renewing || renewPending),
18654
+ disabled: saving[tab] || !dirty[tab] || tab === "basic" && (!name || name.length > 32 || /\s/.test(name)) || tab === "dispatch" && !relayValid || tab === "workspace" && (!directory || renewing || renewPending),
18460
18655
  children: t(saving[tab] ? "settingsSaving" : "messageSave")
18461
18656
  })]
18462
18657
  })
@@ -18744,7 +18939,10 @@ function MessageRow({ message, member, t, onConfigure, menuOpen, onAvatar, onClo
18744
18939
  text: markdown,
18745
18940
  labels
18746
18941
  })
18747
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AttachmentList, { attachments: message.attachments })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
18942
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AttachmentList, {
18943
+ attachments: message.attachments,
18944
+ t
18945
+ })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
18748
18946
  className: ChannelsPage_module_default.card,
18749
18947
  "data-state": card.state,
18750
18948
  children: [
@@ -18901,7 +19099,7 @@ function MemberGroup({ title, members, busy, onPick, onEdit, onRemove, t }) {
18901
19099
  * @param props - framework, injected, and locale shares.
18902
19100
  * @returns the three-column channel view.
18903
19101
  */
18904
- function ChannelsPage({ useChannels, selectRoom, send, reviewTask, createRoom, renameRoom, listModels, listClis, listPersonas, addMember, updateMember, removeMember, setLead, setWorkspacePath, renewRoom, setDispatchMode, decidePermission, retryMessage, editMessage, withdrawMessage, openModelSettings, t }) {
19102
+ function ChannelsPage({ useChannels, selectRoom, send, reviewTask, createRoom, renameRoom, listModels, listClis, listPersonas, addMember, updateMember, removeMember, setLead, setWorkspacePath, renewRoom, setDispatchMode, setMaxRelay, decidePermission, retryMessage, editMessage, withdrawMessage, openModelSettings, t }) {
18905
19103
  const state = useChannels((snapshot) => snapshot);
18906
19104
  const [editing, setEditing] = (0, react.useState)(void 0);
18907
19105
  const [ownNotice, setOwnNotice] = (0, react.useState)(void 0);
@@ -19749,6 +19947,7 @@ function ChannelsPage({ useChannels, selectRoom, send, reviewTask, createRoom, r
19749
19947
  effectiveLead: room.effective_lead_id === void 0 ? state.effectiveLeadId ?? state.leadId ?? "" : room.effective_lead_id ?? "",
19750
19948
  renameRoom,
19751
19949
  setDispatchMode,
19950
+ setMaxRelay,
19752
19951
  setLead,
19753
19952
  setWorkspacePath,
19754
19953
  renewRoom,
@@ -19803,6 +20002,11 @@ const NS = "discord-channels";
19803
20002
  const zh = {
19804
20003
  copyCode: "复制代码",
19805
20004
  copiedCode: "已复制",
20005
+ attachmentDownload: "下载",
20006
+ attachmentCopy: "复制图片",
20007
+ attachmentCopying: "复制中…",
20008
+ attachmentCopied: "已复制",
20009
+ attachmentCopyError: "复制失败,请下载图片后保存或复制。",
19806
20010
  footnotes: "脚注",
19807
20011
  panel: "频道",
19808
20012
  channelSettings: "频道设置",
@@ -19812,6 +20016,8 @@ const zh = {
19812
20016
  settingsSaved: "已保存",
19813
20017
  settingsSaving: "保存中…",
19814
20018
  settingsDiscardConfirm: "还有未保存的改动。关闭将丢弃这些改动,确定关闭吗?",
20019
+ settingsRelay: "接力上限",
20020
+ settingsRelayHint: "Agent 连续接力达到上限后暂停自动接力,等人来 @。6–20 的整数,默认 6。",
19815
20021
  workingDirectory: "工作目录",
19816
20022
  workingDirectoryHint: "请输入已存在的绝对目录。只影响写入范围和相对路径,不影响读取;保存后下次新建会话生效。工作区继承所属频道。",
19817
20023
  renewConfirm: "已更新工作目录,是否立即为该频道重建会话?重建会清空当前对话上下文。",
@@ -20008,6 +20214,11 @@ const en = {
20008
20214
  dispatchMixed: "Mixed — choose models and members for each task",
20009
20215
  copyCode: "Copy code",
20010
20216
  copiedCode: "Copied",
20217
+ attachmentDownload: "Download",
20218
+ attachmentCopy: "Copy image",
20219
+ attachmentCopying: "Copying…",
20220
+ attachmentCopied: "Copied",
20221
+ attachmentCopyError: "Could not copy. Download the image to save or copy it.",
20011
20222
  footnotes: "Footnotes",
20012
20223
  taskTabLabel: "Filter tasks by status",
20013
20224
  taskTabAll: "All",
@@ -20057,6 +20268,8 @@ const en = {
20057
20268
  settingsSaved: "Saved",
20058
20269
  settingsSaving: "Saving…",
20059
20270
  settingsDiscardConfirm: "You have unsaved changes. Closing will discard them. Close anyway?",
20271
+ settingsRelay: "Relay cap",
20272
+ settingsRelayHint: "Agent-to-agent relays stop at this cap until a person @-mentions again. Integer 6–20, default 6.",
20060
20273
  workingDirectory: "Working directory",
20061
20274
  workingDirectoryHint: "Enter an existing absolute directory. This affects write scope and relative paths, not reads. It applies to new sessions; workspaces inherit their channel.",
20062
20275
  renewConfirm: "Working directory updated. Recreate this channel’s sessions now? This clears the current conversation context.",
@@ -20239,6 +20452,7 @@ function apply(ctx) {
20239
20452
  updateMember: channels.actions.updateMember,
20240
20453
  removeMember: channels.actions.removeMember,
20241
20454
  setDispatchMode: channels.actions.setDispatchMode,
20455
+ setMaxRelay: channels.actions.setMaxRelay,
20242
20456
  setLead: channels.actions.setLead,
20243
20457
  setWorkspacePath: channels.actions.setWorkspacePath,
20244
20458
  renewRoom: channels.actions.renewRoom,
@@ -7,7 +7,8 @@
7
7
  // one that was consumed is settled from the Session log. Nothing is re-sent,
8
8
  // so a prompt is executed at most once.
9
9
  import { randomUUID } from 'node:crypto';
10
- import { readFileSync } from 'node:fs';
10
+ import { readFileSync, realpathSync, statSync } from 'node:fs';
11
+ import { basename, isAbsolute, relative, sep } from 'node:path';
11
12
  import { exitTarget } from './team-exit.mjs';
12
13
  import { DEFAULT_DISPATCH_MODE, MAX_TEXT } from './store.mjs';
13
14
  import { ChannelApprovals, DEFAULT_ACTIONS } from './approvals.mjs';
@@ -62,6 +63,54 @@ import { attachmentBlocks, namedAttachmentBlocks, nativeAttachmentBlocks, undeli
62
63
 
63
64
  const CONTEXT_LIMIT = 50;
64
65
  const CLOSE_TIMEOUT_MS = 5_000;
66
+ const MAX_LOCAL_IMAGE_BYTES = 8 * 1024 * 1024;
67
+ const MAX_LOCAL_IMAGES = 10;
68
+ const IMAGE_MIME = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp' };
69
+ // Markdown targets may contain spaces; bare paths end at whitespace or punctuation.
70
+ const LOCAL_IMAGE = /!\[([^\]\n]*)\]\((\/[^)\n]+?\.(?:png|jpe?g|gif|webp))\)|(?<![\w/])\/[^\s<>"'`()[\]{},。;,;]+?\.(?:png|jpe?g|gif|webp)(?![\w/])/gi;
71
+
72
+ /** Only files physically inside this room's working directory may leave the machine. */
73
+ export function localImageReply(text, workspacePath, files, logger) {
74
+ if (!files || !workspacePath || !LOCAL_IMAGE.test(text)) return { text, attachments: [] };
75
+ LOCAL_IMAGE.lastIndex = 0;
76
+ let root;
77
+ try { root = realpathSync(workspacePath); } catch { return { text, attachments: [] }; }
78
+ const attachments = [];
79
+ const result = text.replace(LOCAL_IMAGE, (match, alt, markdownPath) => {
80
+ const path = markdownPath ?? match;
81
+ if (!isAbsolute(path)) return match;
82
+ let target;
83
+ try { target = realpathSync(path); } catch (error) {
84
+ logger?.warn('collab: local image skipped (%s): %s', path, error.message);
85
+ return match;
86
+ }
87
+ const within = relative(root, target);
88
+ if (within === '..' || within.startsWith(`..${sep}`) || isAbsolute(within)) return match;
89
+ if (attachments.length >= MAX_LOCAL_IMAGES) {
90
+ logger?.warn('collab: local image skipped (%s): attachment limit %d', path, MAX_LOCAL_IMAGES);
91
+ return match;
92
+ }
93
+ try {
94
+ const size = statSync(target).size;
95
+ if (size > MAX_LOCAL_IMAGE_BYTES) {
96
+ logger?.warn('collab: local image skipped (%s): exceeds %d bytes', path, MAX_LOCAL_IMAGE_BYTES);
97
+ return match;
98
+ }
99
+ const data = readFileSync(target);
100
+ if (data.length > MAX_LOCAL_IMAGE_BYTES) {
101
+ logger?.warn('collab: local image skipped (%s): exceeds %d bytes', path, MAX_LOCAL_IMAGE_BYTES);
102
+ return match;
103
+ }
104
+ const mime = IMAGE_MIME[path.slice(path.lastIndexOf('.') + 1).toLowerCase()];
105
+ attachments.push(...files.save([{ name: basename(path), mime, data: data.toString('base64') }]));
106
+ return alt ?? '';
107
+ } catch (error) {
108
+ logger?.warn('collab: local image skipped (%s): %s', path, error.message);
109
+ return match;
110
+ }
111
+ });
112
+ return { text: result, attachments };
113
+ }
65
114
  // Agent-to-agent relay cap: after this many agent messages in a row without a
66
115
  // human message, agent mentions stop starting DSH turns (prevents loops with bots).
67
116
  export const DEFAULT_MAX_RELAY = 6;
@@ -461,10 +510,12 @@ export class DshMembers {
461
510
  for (const id of targets) this.enqueue({ member_id: id, room_id: msg.room_id }, () => this.renew(id, msg));
462
511
  };
463
512
  }
464
- if (msg.author_kind === 'role' && this.store.relayDepth(msg.room_id, msg.id) > this.maxRelay) {
513
+ // #445: a channel's saved cap wins; otherwise the instance default (this.maxRelay).
514
+ const cap = this.store.savedMaxRelay?.(msg.room_id) ?? this.maxRelay;
515
+ if (msg.author_kind === 'role' && this.store.relayDepth(msg.room_id, msg.id) > cap) {
465
516
  return () => this.hub.post(msg.room_id, {
466
517
  authorKind: 'system', author: 'system', name: 'system', clientKey: `relay-cap-${msg.id}`,
467
- text: `Agent 之间已经连续接力 ${this.maxRelay} 次以上,暂停自动接力;需要继续请人来 @。`,
518
+ text: `Agent 之间已经连续接力 ${cap} 次以上,暂停自动接力;需要继续请人来 @。`,
468
519
  });
469
520
  }
470
521
  const jobs = targets.map(id => this.store.addJob(id, msg.room_id, msg.id)).filter(Boolean);
@@ -659,8 +710,9 @@ export class DshMembers {
659
710
  if (!text) { this.fail(job.id, `${member.name} 这一轮没有给出回复(${member.cli} 没有输出文字)`); return; }
660
711
  latest = '';
661
712
  const reply = this.returnToLead(job, member, text);
662
- const committed = draft === undefined ? undefined : this.hub.commit(draft.id, 'role', member.id, reply);
663
- const message = committed ?? this.hub.post(job.room_id, { authorKind: 'role', author: member.id, name: member.name, clientKey, text: reply }).message;
713
+ const converted = localImageReply(reply, cwd, this.rt.files, this.rt.logger);
714
+ const committed = draft === undefined ? undefined : this.hub.commit(draft.id, 'role', member.id, converted.text, undefined, converted.attachments);
715
+ const message = committed ?? this.hub.post(job.room_id, { authorKind: 'role', author: member.id, name: member.name, clientKey, text: converted.text, attachments: converted.attachments }).message;
664
716
  done = true;
665
717
  this.store.finishJob(job.id, 'done', message.id);
666
718
  } catch (error) {
@@ -745,9 +797,12 @@ export class DshMembers {
745
797
  }
746
798
  const kind = reason?.kind;
747
799
  const complete = kind === undefined || kind === 'completed' || kind === 'max-tokens';
800
+ const reply = this.returnToLead(job, member, complete ? text : `${text}\n\n(这一轮没有正常结束:${describeReason(reason)})`);
801
+ const cwd = this.store.workspacePath(job.room_id, this.rt.workspacePath);
802
+ const converted = localImageReply(reply, cwd, this.rt.files, this.rt.logger);
748
803
  const { message } = this.hub.post(job.room_id, {
749
804
  authorKind: 'role', author: member.id, name: member.name, clientKey: `dsh-job-${job.id}`,
750
- text: this.returnToLead(job, member, complete ? text : `${text}\n\n(这一轮没有正常结束:${describeReason(reason)})`),
805
+ text: converted.text, attachments: converted.attachments,
751
806
  });
752
807
  this.store.finishJob(job.id, 'done', message.id);
753
808
  return complete;
@@ -63,6 +63,8 @@ function run(c, lines, lead, key = 'msg') {
63
63
  case 'kinds': {
64
64
  const found = captures(sections(lines, c.section, lead), new RegExp(c.line_re), lead);
65
65
  const got = new Set(found);
66
+ const mx = c.max, n = mx ? found.filter(k => k === mx.kind).length : 0;
67
+ if (mx && n > mx.n) return mx.msg.replace('{n}', String(n));
66
68
  const miss = c.need.filter(k => !got.has(k));
67
69
  return miss.length ? missing(c, miss) : null;
68
70
  }
@@ -77,6 +79,37 @@ function run(c, lines, lead, key = 'msg') {
77
79
  const rx = new RegExp(`^${escape(c.field)}\\s*[::]\\s*\\S`);
78
80
  return lines.some(l => rx.test(stripLead(l, lead))) ? null : c[key];
79
81
  }
82
+ case 'art_ref': {
83
+ const trig = new RegExp(c.trigger_re);
84
+ if (!c.trigger_sections.some(s => sections(lines, s, lead).some(seg => seg.some(x => trig.test(x))))) return null;
85
+ // The scope changes code files: work on image-related code, not image work.
86
+ const code = new RegExp(c.code_line_re);
87
+ if (sections(lines, c.code_section, lead).some(seg => seg.slice(1).some(x => code.test(stripLead(x, lead))))) return null;
88
+ const refs = new Set(sections(lines, c.section, lead, Boolean(c.inline))
89
+ .flatMap(seg => seg.flatMap(x => [...x.matchAll(new RegExp(c.image_re, 'g'))].map(m => m[0]))));
90
+ if (refs.size < c.min) return c.msg.replaceAll('{min}', String(c.min)).replaceAll('{n}', String(refs.size));
91
+ const all = lines.join('\n');
92
+ if (new RegExp(c.original_re).test(all) && !new RegExp(c.sample_re).test(all)) return c.original_msg;
93
+ return null;
94
+ }
95
+ case 'section_needs_table': {
96
+ const body = sections(lines, c.trigger_section, lead).flatMap(seg => seg.slice(1)).join('\n');
97
+ if (!(new RegExp(c.trigger_re).test(body) && new RegExp(c.object_re).test(body))) return null;
98
+ const row = new RegExp(c.row_re);
99
+ for (const seg of sections(lines, c.section, lead)) {
100
+ const rest = seg.slice(1), text = rest.join('\n');
101
+ if (rest.filter(x => row.test(stripLead(x, lead))).length >= c.rows && c.need_res.every(r => new RegExp(r).test(text))) return null;
102
+ }
103
+ return c[key];
104
+ }
105
+ case 'forbid': {
106
+ const rx = new RegExp(c.re);
107
+ return lines.some(x => rx.test(x)) ? c[key] : null;
108
+ }
109
+ case 'needs_if': {
110
+ const text = lines.join('\n');
111
+ return new RegExp(c.trigger_re).test(text) && !new RegExp(c.need_re).test(text) ? c[key] : null;
112
+ }
80
113
  default: throw new HandoffRulesError(`交接规则文件里有不认识的检查类型:${c.type}`);
81
114
  }
82
115
  }
package/collab/hub.mjs CHANGED
@@ -162,10 +162,10 @@ export class Hub {
162
162
  return r;
163
163
  }
164
164
 
165
- commit(id, authorKind, author, text, fromTurn) {
165
+ commit(id, authorKind, author, text, fromTurn, attachments) {
166
166
  const after = [];
167
167
  const r = this.store.transaction(() => {
168
- const c = this.store.commit(id, authorKind, author, text, fromTurn);
168
+ const c = this.store.commit(id, authorKind, author, text, fromTurn, attachments);
169
169
  if (c?.first) for (const plan of this.planners) { const next = plan(c.message); if (next) after.push(next); }
170
170
  return c;
171
171
  });
package/collab/plugin.mjs CHANGED
@@ -102,7 +102,8 @@ export async function apply(ctx, rawConfig) {
102
102
  const { WebSocketServer } = require('ws');
103
103
  if (typeof createUserMessage !== 'function' || typeof WebSocketServer !== 'function') throw Error('conest-collab: unsupported DSH runtime');
104
104
 
105
- const store = new Store(join(config.dataDir, 'collab.sqlite3'));
105
+ // #445: the instance maxRelay stays the default for channels that never set their own cap.
106
+ const store = new Store(join(config.dataDir, 'collab.sqlite3'), { maxRelay: config.maxRelay });
106
107
  for (const room of config.rooms) {
107
108
  store.ensureRoom(room.id, store.room(room.id)?.name ?? room.name.trim(), room.workspacePath);
108
109
  store.ensureRoomSettings(room.id);
@@ -400,7 +401,7 @@ export function roomSettingsRoute(req, res, path, { store, hub, dsh, workspacePa
400
401
  const roomId = Number(match[1]);
401
402
  if (store.room(roomId) === undefined) return Promise.resolve(json(res, 404, { error: '频道不存在' }));
402
403
  const view = () => ({ lead_id: store.leadId(roomId), effective_lead_id: hub.primaryIn(roomId)?.id ?? null,
403
- workspace_path: store.workspacePath(roomId, workspacePath) });
404
+ workspace_path: store.workspacePath(roomId, workspacePath), max_relay: store.relaySetting(roomId).max_relay });
404
405
  if (match[2] === 'renew' && req.method === 'POST') {
405
406
  return withBody(req, res, async () => ({ members: await dsh.renewRoom(roomId) }), false);
406
407
  }
@@ -421,6 +422,8 @@ export function roomSettingsRoute(req, res, path, { store, hub, dsh, workspacePa
421
422
  store.transaction(() => {
422
423
  if (body?.lead_id !== undefined) store.setLead(roomId, body.lead_id);
423
424
  if (body?.workspace_path !== undefined) store.setWorkspacePath(roomId, body.workspace_path, workspacePath);
425
+ // #445: 6–20 only; an invalid value rolls the whole PATCH back.
426
+ if (body?.max_relay !== undefined) store.setMaxRelay(roomId, body.max_relay);
424
427
  });
425
428
  hub.emit({ type: 'room-settings', room_id: store.settingsRoom(roomId), ...view() });
426
429
  return view();
package/collab/store.mjs CHANGED
@@ -10,6 +10,11 @@ export const MAX_TEXT = 128 * 1024;
10
10
  // 「默认混合模型」); the same names as the bridge's dispatch_mode config.
11
11
  export const DISPATCH_MODES = ['solo', 'team', 'mixed'];
12
12
  export const DEFAULT_DISPATCH_MODE = 'mixed';
13
+ // #445: a chat channel may set its own relay cap; the Issue's acceptance fixes the range
14
+ // (5/21 rejected, 6/20 accepted) and the default (6).
15
+ export const MIN_CHANNEL_MAX_RELAY = 6;
16
+ export const MAX_CHANNEL_MAX_RELAY = 20;
17
+ export const DEFAULT_CHANNEL_MAX_RELAY = 6;
13
18
  // v7 is #133 (reply_to), v8 is #139 team mode A, v9 is #139 team mode B tasks, v10 is #177
14
19
  // (room_settings.dispatch_explicit); columns are added by ensureSchema() on every open
15
20
  // (checked with PRAGMA table_info), so either merge or release order ends with every column;
@@ -66,7 +71,13 @@ export const WORKSPACE_ID_MIN = 1000000;
66
71
  export const QUOTE_CHARS = 40;
67
72
 
68
73
  export class Store {
69
- constructor(path) {
74
+ /**
75
+ * @param path - SQLite file, or ':memory:'.
76
+ * @param options - { maxRelay? } instance default for channels that never set one (#445);
77
+ * kept configurable so instances that run a tighter loop keep working.
78
+ */
79
+ constructor(path, { maxRelay = DEFAULT_CHANNEL_MAX_RELAY } = {}) {
80
+ this.relayDefault = maxRelay;
70
81
  if (path !== ':memory:') mkdirSync(dirname(path), { recursive: true });
71
82
  this.db = new DatabaseSync(path);
72
83
  this.closed = false;
@@ -92,7 +103,7 @@ export class Store {
92
103
  byKey: this.db.prepare('SELECT * FROM messages WHERE client_key = ?'),
93
104
  byId: this.db.prepare('SELECT * FROM messages WHERE id = ? AND deleted = 0'),
94
105
  edit: this.db.prepare('UPDATE messages SET text = ?, card = ?, changed_at = ? WHERE id = ? AND author_kind = ? AND author = ? AND deleted = 0 RETURNING *'),
95
- commit: this.db.prepare('UPDATE messages SET text = ?, from_turn = ?, draft = 0, changed_at = ? WHERE id = ? AND author_kind = ? AND author = ? AND deleted = 0 AND card IS NULL AND draft = 1 RETURNING *'),
106
+ commit: this.db.prepare('UPDATE messages SET text = ?, from_turn = ?, attachments = ?, draft = 0, changed_at = ? WHERE id = ? AND author_kind = ? AND author = ? AND deleted = 0 AND card IS NULL AND draft = 1 RETURNING *'),
96
107
  editUser: this.db.prepare(`UPDATE messages SET text = ?, edited_at = ? WHERE id = ? AND author_kind = 'user' AND author = ?
97
108
  AND deleted = 0 AND draft = 0 AND card IS NULL RETURNING *`),
98
109
  del: this.db.prepare('UPDATE messages SET deleted = 1 WHERE id = ? AND author_kind = ? AND author = ? AND deleted = 0 RETURNING *'),
@@ -188,6 +199,8 @@ export class Store {
188
199
  }
189
200
  if (!settingColumns.some(c => c.name === 'lead_id')) this.db.exec('ALTER TABLE room_settings ADD COLUMN lead_id TEXT');
190
201
  if (!settingColumns.some(c => c.name === 'workspace_path')) this.db.exec('ALTER TABLE room_settings ADD COLUMN workspace_path TEXT');
202
+ // #445: NULL means the channel never set a cap, so the instance default applies.
203
+ if (!settingColumns.some(c => c.name === 'max_relay')) this.db.exec('ALTER TABLE room_settings ADD COLUMN max_relay INTEGER');
191
204
  this.db.exec('DROP TABLE IF EXISTS collab_settings');
192
205
  if (version < SCHEMA_VERSION) this.db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
193
206
  });
@@ -540,6 +553,28 @@ export class Store {
540
553
  return mode;
541
554
  }
542
555
 
556
+ /** A channel's saved relay cap (a workspace reads its chat channel); null when it never set one (#445). */
557
+ savedMaxRelay(roomId) {
558
+ const row = this.db.prepare('SELECT max_relay FROM room_settings WHERE room_id = ?').get(this.settingsRoom(roomId));
559
+ return row?.max_relay ?? null;
560
+ }
561
+
562
+ /** The setting the client shows: the saved cap, else the instance default (#445). */
563
+ relaySetting(roomId) {
564
+ return { max_relay: this.savedMaxRelay(roomId) ?? this.relayDefault };
565
+ }
566
+
567
+ /** Set a chat channel's relay cap; only 6–20 (Issue #445), stored on the channel so its workspaces share it. */
568
+ setMaxRelay(roomId, value) {
569
+ if (!Number.isInteger(value) || value < MIN_CHANNEL_MAX_RELAY || value > MAX_CHANNEL_MAX_RELAY) {
570
+ throw new CollabError('invalid', `接力上限必须是 ${MIN_CHANNEL_MAX_RELAY}–${MAX_CHANNEL_MAX_RELAY} 的整数`);
571
+ }
572
+ roomId = this.settingsRoom(roomId); // set on a workspace = set on its chat channel
573
+ this.ensureRoomSettings(roomId);
574
+ this.db.prepare('UPDATE room_settings SET max_relay = ? WHERE room_id = ?').run(value, roomId);
575
+ return value;
576
+ }
577
+
543
578
  /** Keep legacy members visible in all channels, including future ones. */
544
579
  migrateMemberRooms() {
545
580
  return this.db.prepare("UPDATE members SET rooms = '[]' WHERE rooms IS NULL").run().changes > 0;
@@ -605,7 +640,7 @@ export class Store {
605
640
  this.releaseIdleWorkspaces();
606
641
  const counts = new Map(this.db.prepare(`SELECT room_id, COUNT(*) AS n FROM tasks
607
642
  WHERE status IN ('assigned', 'accepted', 'asked', 'rework') GROUP BY room_id`).all().map(t => [t.room_id, t.n]));
608
- return this.rooms().map(r => ({ ...r, active: counts.get(r.id) ?? 0, ...this.dispatchSetting(r.id) }));
643
+ return this.rooms().map(r => ({ ...r, active: counts.get(r.id) ?? 0, ...this.dispatchSetting(r.id), ...this.relaySetting(r.id) }));
609
644
  }
610
645
 
611
646
  /**
@@ -664,9 +699,9 @@ export class Store {
664
699
  * into a reply (the caller fans out once). A row already committed gets the
665
700
  * text and first = false. undefined if it is not that author's live text row.
666
701
  */
667
- commit(id, authorKind, author, text, fromTurn, now = Date.now() / 1000) {
668
- if (!text.trim() || text.length > MAX_TEXT) return undefined;
669
- const row = this.q.commit.get(text, fromTurn == null ? null : (fromTurn ? 1 : 0), now, id, authorKind, author);
702
+ commit(id, authorKind, author, text, fromTurn, attachments, now = Date.now() / 1000) {
703
+ if ((!text.trim() && !attachments?.length) || text.length > MAX_TEXT) return undefined;
704
+ const row = this.q.commit.get(text, fromTurn == null ? null : (fromTurn ? 1 : 0), attachments?.length ? JSON.stringify(attachments) : null, now, id, authorKind, author);
670
705
  if (row !== undefined) return { message: row, first: true };
671
706
  const old = this.message(id);
672
707
  if (old === undefined || old.card != null) return undefined;
@@ -17,11 +17,13 @@ const SELF_CHECK = new RegExp(RULES.ask.self_check_re);
17
17
  const CATEGORY = new RegExp(RULES.ask.category_re);
18
18
  const EXEMPT = new RegExp(RULES.ask.exempt_re);
19
19
  const NO_CHECK = new RegExp(RULES.ask.no_check_re);
20
+ const JARGON_URL = new RegExp(RULES.jargon.url_re, 'g');
20
21
  const JARGON_LINE = new RegExp(RULES.jargon.line_re, 'g');
21
22
  const JARGON_TABLE = new RegExp(RULES.jargon.table_re);
22
23
  const BACKGROUND = new RegExp(RULES.summary.background_re);
23
24
  const TODO = new RegExp(RULES.summary.todo_re);
24
25
  const DETAIL = new RegExp(RULES.summary.detail_re);
26
+ const ADDED = new RegExp(RULES.summary.added_re);
25
27
  // Consecutive bounces that still wake the asking member; later ones are only posted.
26
28
  const BOUNCE_WAKE_LIMIT = RULES.bounce.wake_limit;
27
29
 
@@ -56,7 +58,7 @@ export function exitHeadline(body) {
56
58
 
57
59
  /**
58
60
  * { summary, detail } of a marked body (Go: exitSplit): after the first line's #id, up to and
59
- * including the 「要你做的:」 line is posted for the user; the lines after it go under that post.
61
+ * including the 「我加的:」 line (else the 「要你做的:」 line) is posted for the user; the lines after it go under that post.
60
62
  */
61
63
  export function exitSplit(body) {
62
64
  let lines = String(body ?? '').replaceAll('\r\n', '\n').split('\n');
@@ -64,6 +66,10 @@ export function exitSplit(body) {
64
66
  if (m) lines[0] = m[2];
65
67
  while (lines.length && !lines[0].trim()) lines.shift();
66
68
  let end = lines.findIndex(l => TODO.test(l));
69
+ if (end >= 0) {
70
+ const added = lines.findIndex((l, i) => i > end && ADDED.test(l));
71
+ if (added >= 0) end = added;
72
+ }
67
73
  if (end < 0) {
68
74
  const blank = lines.findIndex(l => !l.trim());
69
75
  end = blank < 0 ? lines.length - 1 : blank - 1;
@@ -75,7 +81,7 @@ export function exitSplit(body) {
75
81
  return { summary, detail: rest.join('\n').trim() };
76
82
  }
77
83
 
78
- /** What the part for the user lacks: background, what was done, what the user is to do (Go: summaryProblems). */
84
+ /** What the part for the user lacks: background, what was done, what the user is to do, what the agent added (Go: summaryProblems). */
79
85
  export function summaryProblems(summary) {
80
86
  const lines = String(summary).split('\n');
81
87
  const problems = [];
@@ -90,13 +96,14 @@ export function summaryProblems(summary) {
90
96
  }
91
97
  if (!done) problems.push(RULES.summary.missing_done);
92
98
  if (todo < 0) problems.push(RULES.summary.missing_todo);
99
+ if (!lines.some(l => ADDED.test(l))) problems.push(RULES.summary.missing_added);
93
100
  return problems;
94
101
  }
95
102
 
96
103
  /** The part for the user names numbers, workspaces, branches, paths or identifiers, or has a table (Go: jargonProblems). */
97
104
  export function jargonProblems(line) {
98
105
  const problems = [];
99
- const hits = [...String(line).matchAll(JARGON_LINE)].map(m => m[0].replace(/^[^~/`#!_\-0-9A-Za-z#!工]+/u, ''));
106
+ const hits = [...String(line).replace(JARGON_URL, ' ').matchAll(JARGON_LINE)].map(m => m[0].replace(/^[^~/`#!_\-0-9A-Za-z#!工]+/u, ''));
100
107
  if (hits.length) problems.push(RULES.jargon.line.replaceAll('{hits}', hits.join('、')));
101
108
  if (JARGON_TABLE.test(line)) problems.push(RULES.jargon.table);
102
109
  return problems;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bothouse-dsh",
3
- "version": "0.1.42",
3
+ "version": "0.1.44",
4
4
  "description": "BotHouse channels for DeepSeek Harness: Discord-style rooms where DSH models and local CLIs (Claude Code, Codex, Gemini) are members",
5
5
  "type": "module",
6
6
  "main": "./collab/plugin.mjs",