bothouse-dsh 0.1.41 → 0.1.43

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: 79a9d7898c26fd8a4dbb904f053c9288ca8683a349abf407d1ba53a8ec300f85
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;
@@ -16413,6 +16413,7 @@ var ChannelsPage_module_default = {
16413
16413
  "activeBusy": "dshc_activeBusy",
16414
16414
  "activeCount": "dshc_activeCount",
16415
16415
  "addMemberButton": "dshc_addMemberButton",
16416
+ "attachmentActions": "dshc_attachmentActions",
16416
16417
  "attachmentAudio": "dshc_attachmentAudio",
16417
16418
  "attachmentFile": "dshc_attachmentFile",
16418
16419
  "attachmentImage": "dshc_attachmentImage",
@@ -16475,6 +16476,12 @@ var ChannelsPage_module_default = {
16475
16476
  "guide": "dshc_guide",
16476
16477
  "hash": "dshc_hash",
16477
16478
  "iconButton": "dshc_iconButton",
16479
+ "imageAttachment": "dshc_imageAttachment",
16480
+ "imagePreviewActions": "dshc_imagePreviewActions",
16481
+ "imagePreviewDialog": "dshc_imagePreviewDialog",
16482
+ "imagePreviewHeader": "dshc_imagePreviewHeader",
16483
+ "imagePreviewImage": "dshc_imagePreviewImage",
16484
+ "imagePreviewTrigger": "dshc_imagePreviewTrigger",
16478
16485
  "input": "dshc_input",
16479
16486
  "linkButton": "dshc_linkButton",
16480
16487
  "log": "dshc_log",
@@ -16570,22 +16577,163 @@ var ChannelsPage_module_default = {
16570
16577
  const isImage = (mime) => /^image\/(png|jpeg|gif|webp)$/.test(mime);
16571
16578
  const isAudio = (mime) => mime.startsWith("audio/");
16572
16579
  const size = (bytes) => bytes < 1024 ? `${bytes} B` : bytes < 1048576 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1024 / 1024).toFixed(1)} MB`;
16580
+ function ImageAttachment({ attachment: a, t }) {
16581
+ const [opened, setOpened] = (0, react.useState)(false);
16582
+ const trigger = (0, react.useRef)(null);
16583
+ const dialog = (0, react.useRef)(null);
16584
+ const closeButton = (0, react.useRef)(null);
16585
+ (0, react.useEffect)(() => {
16586
+ if (!opened) return;
16587
+ const origin = trigger.current;
16588
+ closeButton.current?.focus();
16589
+ const containFocus = (event) => {
16590
+ if (!dialog.current?.contains(event.target)) closeButton.current?.focus();
16591
+ };
16592
+ document.addEventListener("focusin", containFocus);
16593
+ return () => {
16594
+ document.removeEventListener("focusin", containFocus);
16595
+ origin?.focus();
16596
+ };
16597
+ }, [opened]);
16598
+ const [copyState, setCopyState] = (0, react.useState)("idle");
16599
+ (0, react.useEffect)(() => {
16600
+ if (copyState !== "copied") return;
16601
+ const timer = setTimeout(() => setCopyState("idle"), 2e3);
16602
+ return () => clearTimeout(timer);
16603
+ }, [copyState]);
16604
+ const copy = async () => {
16605
+ if (copyState === "copying") return;
16606
+ setCopyState("copying");
16607
+ try {
16608
+ if (typeof navigator.clipboard?.write !== "function" || typeof ClipboardItem === "undefined") {
16609
+ setCopyState("error");
16610
+ return;
16611
+ }
16612
+ const response = await fetch(attachmentUrl(a.id));
16613
+ if (!response.ok) throw new Error("Image download failed");
16614
+ const blob = await response.blob();
16615
+ await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
16616
+ setCopyState("copied");
16617
+ } catch {
16618
+ setCopyState("error");
16619
+ }
16620
+ };
16621
+ const copyLabel = t(copyState === "copied" ? "attachmentCopied" : copyState === "copying" ? "attachmentCopying" : "attachmentCopy");
16622
+ const actions = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
16623
+ className: copyState === "error" ? ChannelsPage_module_default.primaryButton : ChannelsPage_module_default.secondaryButton,
16624
+ href: attachmentUrl(a.id),
16625
+ download: a.name,
16626
+ target: "_blank",
16627
+ rel: "noreferrer",
16628
+ "aria-label": t("attachmentDownload"),
16629
+ children: t("attachmentDownload")
16630
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
16631
+ type: "button",
16632
+ className: ChannelsPage_module_default.secondaryButton,
16633
+ onClick: () => {
16634
+ copy();
16635
+ },
16636
+ disabled: copyState === "copying",
16637
+ "aria-label": copyLabel,
16638
+ "aria-live": "polite",
16639
+ children: copyLabel
16640
+ })] });
16641
+ const error = copyState === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
16642
+ className: ChannelsPage_module_default.sendError,
16643
+ role: "alert",
16644
+ children: t("attachmentCopyError")
16645
+ });
16646
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
16647
+ className: ChannelsPage_module_default.imageAttachment,
16648
+ "data-copy-state": copyState,
16649
+ children: [
16650
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
16651
+ ref: trigger,
16652
+ type: "button",
16653
+ className: ChannelsPage_module_default.imagePreviewTrigger,
16654
+ title: a.name,
16655
+ "aria-haspopup": "dialog",
16656
+ "aria-expanded": opened,
16657
+ onClick: () => setOpened(true),
16658
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
16659
+ className: ChannelsPage_module_default.attachmentImage,
16660
+ src: attachmentUrl(a.id),
16661
+ alt: a.name,
16662
+ loading: "lazy"
16663
+ })
16664
+ }),
16665
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
16666
+ className: ChannelsPage_module_default.attachmentActions,
16667
+ children: actions
16668
+ }),
16669
+ !opened && error,
16670
+ opened && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
16671
+ className: ChannelsPage_module_default.dialogBackdrop,
16672
+ onClick: (event) => {
16673
+ if (event.target === event.currentTarget) setOpened(false);
16674
+ },
16675
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
16676
+ ref: dialog,
16677
+ className: `${ChannelsPage_module_default.dialog} ${ChannelsPage_module_default.imagePreviewDialog}`,
16678
+ role: "dialog",
16679
+ "aria-modal": "true",
16680
+ "aria-label": a.name,
16681
+ tabIndex: -1,
16682
+ onKeyDown: (event) => {
16683
+ if (event.key === "Escape") {
16684
+ event.preventDefault();
16685
+ event.stopPropagation();
16686
+ setOpened(false);
16687
+ }
16688
+ if (event.key === "Tab") {
16689
+ const controls = Array.from(event.currentTarget.querySelectorAll("button:not(:disabled), a[href]"));
16690
+ const first = controls[0];
16691
+ const last = controls[controls.length - 1];
16692
+ if (event.shiftKey && (document.activeElement === first || document.activeElement === dialog.current)) {
16693
+ event.preventDefault();
16694
+ last?.focus();
16695
+ } else if (!event.shiftKey && (document.activeElement === last || document.activeElement === dialog.current)) {
16696
+ event.preventDefault();
16697
+ first?.focus();
16698
+ }
16699
+ }
16700
+ },
16701
+ children: [
16702
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
16703
+ className: ChannelsPage_module_default.imagePreviewHeader,
16704
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: a.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
16705
+ ref: closeButton,
16706
+ type: "button",
16707
+ className: ChannelsPage_module_default.secondaryButton,
16708
+ "aria-label": t("close"),
16709
+ onClick: () => setOpened(false),
16710
+ children: "×"
16711
+ })]
16712
+ }),
16713
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
16714
+ className: ChannelsPage_module_default.imagePreviewImage,
16715
+ src: attachmentUrl(a.id),
16716
+ alt: a.name
16717
+ }),
16718
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
16719
+ className: ChannelsPage_module_default.imagePreviewActions,
16720
+ children: actions
16721
+ }),
16722
+ error
16723
+ ]
16724
+ })
16725
+ }), document.body)
16726
+ ]
16727
+ });
16728
+ }
16573
16729
  /** Thumbnails for images, a download card for other files. */
16574
- function AttachmentList({ attachments }) {
16730
+ function AttachmentList({ attachments, t }) {
16575
16731
  if (attachments === void 0 || attachments.length === 0) return null;
16576
16732
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
16577
16733
  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
- })
16734
+ children: attachments.map((a) => isImage(a.mime) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageAttachment, {
16735
+ attachment: a,
16736
+ t
16589
16737
  }, a.id) : isAudio(a.mime) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
16590
16738
  className: ChannelsPage_module_default.attachmentAudio,
16591
16739
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("audio", {
@@ -18744,7 +18892,10 @@ function MessageRow({ message, member, t, onConfigure, menuOpen, onAvatar, onClo
18744
18892
  text: markdown,
18745
18893
  labels
18746
18894
  })
18747
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AttachmentList, { attachments: message.attachments })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
18895
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AttachmentList, {
18896
+ attachments: message.attachments,
18897
+ t
18898
+ })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
18748
18899
  className: ChannelsPage_module_default.card,
18749
18900
  "data-state": card.state,
18750
18901
  children: [
@@ -19803,6 +19954,11 @@ const NS = "discord-channels";
19803
19954
  const zh = {
19804
19955
  copyCode: "复制代码",
19805
19956
  copiedCode: "已复制",
19957
+ attachmentDownload: "下载",
19958
+ attachmentCopy: "复制图片",
19959
+ attachmentCopying: "复制中…",
19960
+ attachmentCopied: "已复制",
19961
+ attachmentCopyError: "复制失败,请下载图片后保存或复制。",
19806
19962
  footnotes: "脚注",
19807
19963
  panel: "频道",
19808
19964
  channelSettings: "频道设置",
@@ -20008,6 +20164,11 @@ const en = {
20008
20164
  dispatchMixed: "Mixed — choose models and members for each task",
20009
20165
  copyCode: "Copy code",
20010
20166
  copiedCode: "Copied",
20167
+ attachmentDownload: "Download",
20168
+ attachmentCopy: "Copy image",
20169
+ attachmentCopying: "Copying…",
20170
+ attachmentCopied: "Copied",
20171
+ attachmentCopyError: "Could not copy. Download the image to save or copy it.",
20011
20172
  footnotes: "Footnotes",
20012
20173
  taskTabLabel: "Filter tasks by status",
20013
20174
  taskTabAll: "All",
@@ -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;
@@ -659,8 +708,9 @@ export class DshMembers {
659
708
  if (!text) { this.fail(job.id, `${member.name} 这一轮没有给出回复(${member.cli} 没有输出文字)`); return; }
660
709
  latest = '';
661
710
  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;
711
+ const converted = localImageReply(reply, cwd, this.rt.files, this.rt.logger);
712
+ const committed = draft === undefined ? undefined : this.hub.commit(draft.id, 'role', member.id, converted.text, undefined, converted.attachments);
713
+ 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
714
  done = true;
665
715
  this.store.finishJob(job.id, 'done', message.id);
666
716
  } catch (error) {
@@ -745,9 +795,12 @@ export class DshMembers {
745
795
  }
746
796
  const kind = reason?.kind;
747
797
  const complete = kind === undefined || kind === 'completed' || kind === 'max-tokens';
798
+ const reply = this.returnToLead(job, member, complete ? text : `${text}\n\n(这一轮没有正常结束:${describeReason(reason)})`);
799
+ const cwd = this.store.workspacePath(job.room_id, this.rt.workspacePath);
800
+ const converted = localImageReply(reply, cwd, this.rt.files, this.rt.logger);
748
801
  const { message } = this.hub.post(job.room_id, {
749
802
  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)})`),
803
+ text: converted.text, attachments: converted.attachments,
751
804
  });
752
805
  this.store.finishJob(job.id, 'done', message.id);
753
806
  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
  }
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/store.mjs CHANGED
@@ -92,7 +92,7 @@ export class Store {
92
92
  byKey: this.db.prepare('SELECT * FROM messages WHERE client_key = ?'),
93
93
  byId: this.db.prepare('SELECT * FROM messages WHERE id = ? AND deleted = 0'),
94
94
  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 *'),
95
+ 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
96
  editUser: this.db.prepare(`UPDATE messages SET text = ?, edited_at = ? WHERE id = ? AND author_kind = 'user' AND author = ?
97
97
  AND deleted = 0 AND draft = 0 AND card IS NULL RETURNING *`),
98
98
  del: this.db.prepare('UPDATE messages SET deleted = 1 WHERE id = ? AND author_kind = ? AND author = ? AND deleted = 0 RETURNING *'),
@@ -664,9 +664,9 @@ export class Store {
664
664
  * into a reply (the caller fans out once). A row already committed gets the
665
665
  * text and first = false. undefined if it is not that author's live text row.
666
666
  */
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);
667
+ commit(id, authorKind, author, text, fromTurn, attachments, now = Date.now() / 1000) {
668
+ if ((!text.trim() && !attachments?.length) || text.length > MAX_TEXT) return undefined;
669
+ const row = this.q.commit.get(text, fromTurn == null ? null : (fromTurn ? 1 : 0), attachments?.length ? JSON.stringify(attachments) : null, now, id, authorKind, author);
670
670
  if (row !== undefined) return { message: row, first: true };
671
671
  const old = this.message(id);
672
672
  if (old === undefined || old.card != null) return undefined;
@@ -1,23 +1,30 @@
1
1
  // Durable workspace questions; system forwards never enter agent routing.
2
2
  import { randomBytes } from 'node:crypto';
3
+ import { readFileSync } from 'node:fs';
3
4
  import { withoutLeadingMentions } from './mentions.mjs';
4
5
 
5
6
  export function exitTarget(msg) {
6
7
  return /^team-answer:([^:]+):/.exec(msg.client_key ?? '')?.[1];
7
8
  }
8
9
 
9
- // #574 B4: an [ask] must carry 「自验:」 (evidence) and 「类别:」 before it waits on
10
- // the user; only 方向、花钱、需要用户看的成品、权限确认 may write 「自验:无需」.
11
- // Same check in the Go bridge: bridge/core/team_ask_evidence.go askEvidenceProblems.
12
- const SELF_CHECK = /自验\s*[::]/;
13
- const CATEGORY = /类别\s*[::]/;
14
- const EXEMPT = /方向|花钱|成品|权限/;
15
- const NO_CHECK = /^(无需|不需要|无|免)([\s,。,.;;((]|$)/;
16
- export const ASK_MISSING_SELF_CHECK = '缺「自验:」(附证据:截图或命令输出)';
17
- export const ASK_MISSING_CATEGORY = '缺「类别:」(方向、花钱、需要用户看的成品、权限确认之一,或写别的类别)';
18
- export const ASK_NO_CHECK_NOT_EXEMPT = '「自验:无需」只限方向、花钱、需要用户看的成品、权限确认四类,其它类别要附证据';
10
+ // The workspace [ask]/[report] rules (first-line mark and #id format, the [ask] self-check, bounce
11
+ // texts) are one file, team-exit-rules.json next to this one; the Go bridge reads the same file
12
+ // (bridge/core/team_exit_rules.go, from its dsh_bundle copy) and both sides run the shared cases in
13
+ // test/team-exit-cases.json. No formats or texts live here. Regexes are written for both RE2 and JS.
14
+ export const RULES = JSON.parse(readFileSync(new URL('./team-exit-rules.json', import.meta.url), 'utf8'));
15
+ const HEADLINE = new RegExp(RULES.headline_re);
16
+ const SELF_CHECK = new RegExp(RULES.ask.self_check_re);
17
+ const CATEGORY = new RegExp(RULES.ask.category_re);
18
+ const EXEMPT = new RegExp(RULES.ask.exempt_re);
19
+ const NO_CHECK = new RegExp(RULES.ask.no_check_re);
20
+ const JARGON_LINE = new RegExp(RULES.jargon.line_re, 'g');
21
+ const JARGON_TABLE = new RegExp(RULES.jargon.table_re);
22
+ const BACKGROUND = new RegExp(RULES.summary.background_re);
23
+ const TODO = new RegExp(RULES.summary.todo_re);
24
+ const DETAIL = new RegExp(RULES.summary.detail_re);
25
+ const ADDED = new RegExp(RULES.summary.added_re);
19
26
  // Consecutive bounces that still wake the asking member; later ones are only posted.
20
- const BOUNCE_WAKE_LIMIT = 2;
27
+ const BOUNCE_WAKE_LIMIT = RULES.bounce.wake_limit;
21
28
 
22
29
  /** What an [ask] body lacks; [] means it may go to the user. */
23
30
  export function askEvidenceProblems(body) {
@@ -32,42 +39,119 @@ export function askEvidenceProblems(body) {
32
39
  check = rest.trim();
33
40
  }
34
41
  const problems = [];
35
- if (!check) problems.push(ASK_MISSING_SELF_CHECK);
36
- if (!category) problems.push(ASK_MISSING_CATEGORY);
37
- if (check && category && NO_CHECK.test(check) && !EXEMPT.test(category)) problems.push(ASK_NO_CHECK_NOT_EXEMPT);
42
+ if (!check) problems.push(RULES.ask.missing_self_check);
43
+ if (!category) problems.push(RULES.ask.missing_category);
44
+ if (check && category && NO_CHECK.test(check) && !EXEMPT.test(category)) problems.push(RULES.ask.no_check_not_exempt);
38
45
  return problems;
39
46
  }
40
47
 
41
- export function askBounceText(problems) {
42
- return exitBounceText('ask', problems);
43
- }
44
-
45
- // Headline (user 2026-10-28 「做」): the first line is `[report] #<task id> <one or two sentences
46
- // for the user>`; that sentence is what the chat channel shows first. Same rule in the Go bridge:
47
- // bridge/core/team_exit_headline.go exitHeadline.
48
- const HEADLINE = /^#(\d+)[\s\u3000::,,、.。]*(.*)$/;
49
- export const EXIT_MISSING_ID = '第一行缺任务板编号(#编号)';
50
- export const EXIT_MISSING_LINE = '第一行缺给用户的一两句话';
51
- export const EXIT_UNKNOWN_ID = '任务板上没有这个编号';
52
-
53
- /** { id, line, problems } of a marked body's first line. */
48
+ /** { id, line, problems } of a marked body's first line; line is the part for the user (Go: exitHeadline). */
54
49
  export function exitHeadline(body) {
55
50
  const first = String(body ?? '').split('\n')[0].trim();
56
51
  const m = HEADLINE.exec(first);
57
- if (!m) return { id: 0, line: '', problems: first === '' || first.startsWith('#') ? [EXIT_MISSING_ID, EXIT_MISSING_LINE] : [EXIT_MISSING_ID] };
58
- const line = m[2].trim();
59
- return { id: Number(m[1]), line, problems: line ? [] : [EXIT_MISSING_LINE] };
52
+ const { missing_id: noId, missing_line: noLine } = RULES.headline;
53
+ if (!m) return { id: 0, line: '', problems: first === '' || first.startsWith('#') ? [noId, noLine] : [noId] };
54
+ const { summary: line } = exitSplit(body);
55
+ return { id: Number(m[1]), line, problems: line ? [] : [noLine] };
56
+ }
57
+
58
+ /**
59
+ * { summary, detail } of a marked body (Go: exitSplit): after the first line's #id, up to and
60
+ * including the 「我加的:」 line (else the 「要你做的:」 line) is posted for the user; the lines after it go under that post.
61
+ */
62
+ export function exitSplit(body) {
63
+ let lines = String(body ?? '').replaceAll('\r\n', '\n').split('\n');
64
+ const m = HEADLINE.exec(lines[0].trim());
65
+ if (m) lines[0] = m[2];
66
+ while (lines.length && !lines[0].trim()) lines.shift();
67
+ let end = lines.findIndex(l => TODO.test(l));
68
+ if (end >= 0) {
69
+ const added = lines.findIndex((l, i) => i > end && ADDED.test(l));
70
+ if (added >= 0) end = added;
71
+ }
72
+ if (end < 0) {
73
+ const blank = lines.findIndex(l => !l.trim());
74
+ end = blank < 0 ? lines.length - 1 : blank - 1;
75
+ }
76
+ const summary = lines.slice(0, end + 1).join('\n').trim();
77
+ let rest = lines.slice(end + 1);
78
+ while (rest.length && !rest[0].trim()) rest.shift();
79
+ if (rest.length && DETAIL.test(rest[0].trim())) rest = rest.slice(1);
80
+ return { summary, detail: rest.join('\n').trim() };
81
+ }
82
+
83
+ /** What the part for the user lacks: background, what was done, what the user is to do, what the agent added (Go: summaryProblems). */
84
+ export function summaryProblems(summary) {
85
+ const lines = String(summary).split('\n');
86
+ const problems = [];
87
+ if (!BACKGROUND.test(lines[0].trim())) problems.push(RULES.summary.missing_background);
88
+ const todo = lines.findIndex(l => TODO.test(l));
89
+ let done = false;
90
+ for (let i = 1; i < lines.length && !done; i++) {
91
+ let l = lines[i];
92
+ if (i === todo) l = l.slice(0, TODO.exec(l).index);
93
+ else if (todo >= 0 && i > todo) break;
94
+ done = l.trim() !== '';
95
+ }
96
+ if (!done) problems.push(RULES.summary.missing_done);
97
+ if (todo < 0) problems.push(RULES.summary.missing_todo);
98
+ if (!lines.some(l => ADDED.test(l))) problems.push(RULES.summary.missing_added);
99
+ return problems;
100
+ }
101
+
102
+ /** The part for the user names numbers, workspaces, branches, paths or identifiers, or has a table (Go: jargonProblems). */
103
+ export function jargonProblems(line) {
104
+ const problems = [];
105
+ const hits = [...String(line).matchAll(JARGON_LINE)].map(m => m[0].replace(/^[^~/`#!_\-0-9A-Za-z#!工]+/u, ''));
106
+ if (hits.length) problems.push(RULES.jargon.line.replaceAll('{hits}', hits.join('、')));
107
+ if (JARGON_TABLE.test(line)) problems.push(RULES.jargon.table);
108
+ return problems;
60
109
  }
61
110
 
62
111
  /** Why a workspace [ask]/[report] stayed in the workspace and how to resend it (Go: exitBounceText). */
63
112
  export function exitBounceText(kind, problems) {
64
- let t = `这条 [${kind}] 没有转给用户:${problems.join(';')}。补上后重发:第一行写 [${kind}] #任务板编号 给用户的一两句话(系统原样贴到聊天频道),第二行起写细节`;
65
- if (kind === 'ask') t += ',正文写「自验:」附证据和「类别:」;方向、花钱、需要用户看的成品、权限确认四类可写「自验:无需」';
66
- return `${t}。`;
113
+ const vars = { kind, problems: problems.join(RULES.bounce.sep), extra: RULES.bounce.extra[kind] ?? '' };
114
+ return RULES.bounce.text.replace(/\{(kind|problems|extra)\}/g, (_, k) => vars[k]);
67
115
  }
68
116
 
69
- const FIRST_MARK = /^\[(ask|report)\][ \t]*\n?([\s\S]*\S[\s\S]*)$/i;
70
- const LINE_MARK = /^[ \t]*\[(ask|report)\]/i;
117
+ /**
118
+ * How a marked body is judged (Go: judgeExit): { id, line, problems, legacy }. known(id): the id is a
119
+ * task; bounces: replies in a row already sent back. problems [] means it goes to the chat channel;
120
+ * legacy: the format is still wrong but bounces stopped waking the member (the bridge's chat lead
121
+ * rewrites it; DSH posts it without the headline).
122
+ */
123
+ export function judgeExit(kind, body, { known, bounces = 0 }) {
124
+ const problems = kind === 'ask' ? askEvidenceProblems(body) : [];
125
+ let { id, line, problems: hp } = exitHeadline(body);
126
+ if (!hp.length && !known(id)) hp = [RULES.headline.unknown_id.replaceAll('{id}', String(id))];
127
+ else if (!hp.length) hp = [...summaryProblems(line), ...jargonProblems(line)];
128
+ // Format goes back only while bounces still wake the member; after that the chat lead rewrites it.
129
+ if (hp.length && !problems.length && bounces >= BOUNCE_WAKE_LIMIT) return { id, line, problems, legacy: true };
130
+ return { id, line, problems: [...problems, ...hp], legacy: false };
131
+ }
132
+
133
+ /**
134
+ * The task a workspace reply is about (Go: exitTaskPick): the one its first line names when that one
135
+ * is here (bound to this workspace), else the one bound here last; 0 when none is here.
136
+ * items: [{ id, here, bound }], bound compared as given.
137
+ */
138
+ export function exitTaskPick(named, items) {
139
+ let pick = 0, last;
140
+ for (const it of items) {
141
+ if (!it.here) continue;
142
+ if (named > 0 && it.id === named) return named;
143
+ if (pick === 0 || it.bound > last) { pick = it.id; last = it.bound; }
144
+ }
145
+ return pick;
146
+ }
147
+
148
+ const escapeRe = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
149
+ const MARKS = RULES.kinds.map(k => escapeRe(RULES.mark.replace('{kind}', k))).join('|');
150
+ const FIRST_MARK = new RegExp(`^(${MARKS})([\\s\\S]*)$`, 'i');
151
+ // A mark with nothing after it is no mark (Go: parseExitKind).
152
+ const firstMark = t => { const m = FIRST_MARK.exec(t); return m?.[2].trim() ? m : null; };
153
+ const markKind = tag => RULES.kinds.find(k => RULES.mark.replace('{kind}', k).toLowerCase() === tag.toLowerCase());
154
+ const LINE_MARK = new RegExp(`^[ \\t]*(${MARKS})`, 'i');
71
155
 
72
156
  /**
73
157
  * The [ask] / [report] mark of a role reply: { kind, body } or undefined. Marked on the first
@@ -80,15 +164,15 @@ const LINE_MARK = /^[ \t]*\[(ask|report)\]/i;
80
164
  */
81
165
  export function exitMark(text, { lastLine = false, team = [] } = {}) {
82
166
  const t = withoutLeadingMentions(String(text ?? ''), team).trim();
83
- let m = FIRST_MARK.exec(t);
167
+ let m = firstMark(t);
84
168
  if (m === null && lastLine) {
85
169
  const lines = t.split('\n');
86
170
  for (let i = lines.length - 1; i > 0 && m === null; i--) {
87
171
  const line = withoutLeadingMentions(lines[i], team);
88
- if (LINE_MARK.test(line)) m = FIRST_MARK.exec([line.trimStart(), ...lines.slice(i + 1)].join('\n'));
172
+ if (LINE_MARK.test(line)) m = firstMark([line.trimStart(), ...lines.slice(i + 1)].join('\n'));
89
173
  }
90
174
  }
91
- return m === null ? undefined : { kind: m[1].toLowerCase(), body: m[2] };
175
+ return m === null ? undefined : { kind: markKind(m[1]), body: m[2].trim() };
92
176
  }
93
177
 
94
178
  export class TeamExit {
@@ -151,16 +235,11 @@ export class TeamExit {
151
235
  const ask = mark?.kind === 'ask';
152
236
  let headline;
153
237
  if (mark) {
154
- const problems = ask ? askEvidenceProblems(mark.body) : [];
155
- headline = exitHeadline(mark.body);
156
- if (!headline.problems.length) {
157
- headline.task = this.hub.tasks?.task?.(headline.id);
158
- if (!headline.task) headline.problems.push(`${EXIT_UNKNOWN_ID}(#${headline.id})`);
159
- }
238
+ const bounces = this.bounces.get(`${room.id}:${msg.author}`) ?? 0;
239
+ const v = judgeExit(mark.kind, mark.body, { known: id => Boolean(this.hub.tasks?.task?.(id)), bounces });
240
+ if (v.problems.length) return this.bounce(room, msg, v.problems, mark.kind);
160
241
  // Once bounces stop waking the member, a reply still without a headline goes out as before.
161
- if (headline.problems.length && !problems.length && (this.bounces.get(`${room.id}:${msg.author}`) ?? 0) >= BOUNCE_WAKE_LIMIT) headline = undefined;
162
- else problems.push(...headline.problems);
163
- if (problems.length) return this.bounce(room, msg, problems, mark.kind);
242
+ if (!v.legacy) headline = { ...v, task: this.hub.tasks?.task?.(v.id) };
164
243
  }
165
244
  // Any reply without a bounced [ask] ends the run of bounces.
166
245
  this.bounces.delete(`${room.id}:${msg.author}`);
@@ -178,12 +257,12 @@ export class TeamExit {
178
257
  // #597 G35: a DSH member's [ask] keeps its task in work and watched (and its workspace, so the
179
258
  // answer reaches the same session); a bridge bot's [ask] parks it as before (#280).
180
259
  if (tasks && !this.markOpts(msg).lastLine) {
181
- const taskId = headline?.id ?? /任务\s*#(\d+)\b/.exec(mark.body)?.[1];
182
- const inRoom = tasks.q.allBusy.all('assigned', 'accepted', 'asked', 'rework')
183
- .filter(t => t.room_id === room.id);
184
- const marked = taskId ? tasks.task(Number(taskId)) : undefined;
185
- const selected = marked?.room_id === room.id ? marked : inRoom[0];
186
- if (selected?.room_id === room.id) tasks.pauseTask(selected.id);
260
+ const busy = tasks.q.allBusy.all('assigned', 'accepted', 'asked', 'rework');
261
+ const named = headline?.task;
262
+ const items = [...(named ? [named] : []), ...busy]
263
+ .map(t => ({ id: t.id, here: t.room_id === room.id, bound: t.updated_at }));
264
+ const selected = exitTaskPick(named?.id ?? 0, items);
265
+ if (selected) tasks.pauseTask(selected);
187
266
  }
188
267
  }
189
268
  }
@@ -206,16 +285,24 @@ export class TeamExit {
206
285
  */
207
286
  forward(room, msg, body, ask, headline) {
208
287
  let text = `📬 来自 ${room.name} · ${msg.name}${ask ? ` · ${ask.id}` : ' · 阶段结果'}\n${body}`;
288
+ let detail = '';
209
289
  if (headline) {
210
- // The headline as written, named by its task; the details follow (the DSH lead reads them here).
290
+ // The three parts for the user, named by its task; DSH has no threads, so the details are a
291
+ // second post marked as such (Go: postWithDetail).
211
292
  const name = headline.task?.title ?? '';
212
- const rest = String(body).split('\n').slice(1).join('\n').trim();
213
- text = `📬 #${headline.id}「${name}」${ask ? `要你定:${headline.line}(直接回复这条就能答复,编号 ${ask.id})` : `:${headline.line}`}`
214
- + (rest ? `\n\n来自 ${room.name} · ${msg.name} 的细节:\n${rest}` : '');
293
+ detail = exitSplit(body).detail;
294
+ text = `📬「${name}」${ask ? `要你定\n${headline.line}\n(直接回复这条就能答复,编号 ${ask.id})` : `\n${headline.line}`}`;
295
+ if (detail) detail = `📬 细节「${name}」(来自 ${room.name} · ${msg.name},想看再展开,不用回)\n${detail}`;
215
296
  }
216
- return this.hub.post(room.parent_id, {
297
+ const { message } = this.hub.post(room.parent_id, {
217
298
  authorKind: 'system', author: 'system', name: 'system', clientKey: `team-exit:${msg.id}`, text,
218
- }).message;
299
+ });
300
+ if (detail) {
301
+ this.hub.post(room.parent_id, {
302
+ authorKind: 'system', author: 'system', name: 'system', clientKey: `team-exit-detail:${msg.id}`, text: detail,
303
+ });
304
+ }
305
+ return message;
219
306
  }
220
307
 
221
308
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bothouse-dsh",
3
- "version": "0.1.41",
3
+ "version": "0.1.43",
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",