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