feat(messages): add bottom sheet panel mode
This commit is contained in:
parent
02d516cb4e
commit
cc45549716
13 changed files with 1753 additions and 896 deletions
|
|
@ -65,6 +65,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@claude-teams/agent-graph": "workspace:*",
|
||||||
"@codemirror/autocomplete": "^6.20.0",
|
"@codemirror/autocomplete": "^6.20.0",
|
||||||
"@codemirror/commands": "^6.10.2",
|
"@codemirror/commands": "^6.10.2",
|
||||||
"@codemirror/lang-cpp": "^6.0.3",
|
"@codemirror/lang-cpp": "^6.0.3",
|
||||||
|
|
@ -121,7 +122,6 @@
|
||||||
"@xterm/addon-web-links": "^0.12.0",
|
"@xterm/addon-web-links": "^0.12.0",
|
||||||
"@xterm/xterm": "^6.0.0",
|
"@xterm/xterm": "^6.0.0",
|
||||||
"agent-teams-controller": "workspace:*",
|
"agent-teams-controller": "workspace:*",
|
||||||
"@claude-teams/agent-graph": "workspace:*",
|
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|
@ -139,12 +139,14 @@
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
"mdast-util-to-hast": "^13.2.1",
|
"mdast-util-to-hast": "^13.2.1",
|
||||||
"mermaid": "^11.12.3",
|
"mermaid": "^11.12.3",
|
||||||
|
"motion": "12.38.0",
|
||||||
"node-diff3": "^3.2.0",
|
"node-diff3": "^3.2.0",
|
||||||
"node-pty": "^1.1.0",
|
"node-pty": "^1.1.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-grid-layout": "^2.2.2",
|
"react-grid-layout": "^2.2.2",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
|
"react-modal-sheet": "5.6.0",
|
||||||
"react-resizable": "^3.1.3",
|
"react-resizable": "^3.1.3",
|
||||||
"rehype-highlight": "^7.0.2",
|
"rehype-highlight": "^7.0.2",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
|
|
|
||||||
104
pnpm-lock.yaml
104
pnpm-lock.yaml
|
|
@ -230,6 +230,9 @@ importers:
|
||||||
mermaid:
|
mermaid:
|
||||||
specifier: ^11.12.3
|
specifier: ^11.12.3
|
||||||
version: 11.12.3
|
version: 11.12.3
|
||||||
|
motion:
|
||||||
|
specifier: 12.38.0
|
||||||
|
version: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
node-diff3:
|
node-diff3:
|
||||||
specifier: ^3.2.0
|
specifier: ^3.2.0
|
||||||
version: 3.2.0
|
version: 3.2.0
|
||||||
|
|
@ -248,6 +251,9 @@ importers:
|
||||||
react-markdown:
|
react-markdown:
|
||||||
specifier: ^10.1.0
|
specifier: ^10.1.0
|
||||||
version: 10.1.0(@types/react@19.2.14)(react@19.2.4)
|
version: 10.1.0(@types/react@19.2.14)(react@19.2.4)
|
||||||
|
react-modal-sheet:
|
||||||
|
specifier: 5.6.0
|
||||||
|
version: 5.6.0(motion@12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
react-resizable:
|
react-resizable:
|
||||||
specifier: ^3.1.3
|
specifier: ^3.1.3
|
||||||
version: 3.1.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 3.1.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
|
@ -4796,6 +4802,7 @@ packages:
|
||||||
'@xmldom/xmldom@0.8.11':
|
'@xmldom/xmldom@0.8.11':
|
||||||
resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==}
|
resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
|
deprecated: this version has critical issues, please update to the latest version
|
||||||
|
|
||||||
'@xterm/addon-fit@0.11.0':
|
'@xterm/addon-fit@0.11.0':
|
||||||
resolution: {integrity: sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==}
|
resolution: {integrity: sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==}
|
||||||
|
|
@ -6755,6 +6762,20 @@ packages:
|
||||||
fraction.js@5.3.4:
|
fraction.js@5.3.4:
|
||||||
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
|
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
|
||||||
|
|
||||||
|
framer-motion@12.38.0:
|
||||||
|
resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==}
|
||||||
|
peerDependencies:
|
||||||
|
'@emotion/is-prop-valid': '*'
|
||||||
|
react: ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@emotion/is-prop-valid':
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
fresh@0.5.2:
|
fresh@0.5.2:
|
||||||
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
@ -8128,6 +8149,26 @@ packages:
|
||||||
module-details-from-path@1.0.4:
|
module-details-from-path@1.0.4:
|
||||||
resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==}
|
resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==}
|
||||||
|
|
||||||
|
motion-dom@12.38.0:
|
||||||
|
resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==}
|
||||||
|
|
||||||
|
motion-utils@12.36.0:
|
||||||
|
resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==}
|
||||||
|
|
||||||
|
motion@12.38.0:
|
||||||
|
resolution: {integrity: sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==}
|
||||||
|
peerDependencies:
|
||||||
|
'@emotion/is-prop-valid': '*'
|
||||||
|
react: ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@emotion/is-prop-valid':
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
mrmime@2.0.1:
|
mrmime@2.0.1:
|
||||||
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
|
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
@ -9098,6 +9139,13 @@ packages:
|
||||||
'@types/react': '>=18'
|
'@types/react': '>=18'
|
||||||
react: '>=18'
|
react: '>=18'
|
||||||
|
|
||||||
|
react-modal-sheet@5.6.0:
|
||||||
|
resolution: {integrity: sha512-+WE2nVPdB/Jx0QbndIBqGvy6k0IXriW2lFaPeZSW1xOVri6rWhAwrSnArtsR1rxOxW8HBdAYeIPUcbjMvNeeyw==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
peerDependencies:
|
||||||
|
motion: '>=11'
|
||||||
|
react: '>=16'
|
||||||
|
|
||||||
react-refresh@0.17.0:
|
react-refresh@0.17.0:
|
||||||
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
|
resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
@ -9138,6 +9186,15 @@ packages:
|
||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
react-use-measure@2.1.7:
|
||||||
|
resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.13'
|
||||||
|
react-dom: '>=16.13'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
react@18.3.1:
|
react@18.3.1:
|
||||||
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
|
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
@ -15343,14 +15400,6 @@ snapshots:
|
||||||
chai: 5.3.3
|
chai: 5.3.3
|
||||||
tinyrainbow: 2.0.0
|
tinyrainbow: 2.0.0
|
||||||
|
|
||||||
'@vitest/mocker@3.2.4(vite@5.4.21(@types/node@22.19.15)(sass@1.98.0)(terser@5.46.0))':
|
|
||||||
dependencies:
|
|
||||||
'@vitest/spy': 3.2.4
|
|
||||||
estree-walker: 3.0.3
|
|
||||||
magic-string: 0.30.21
|
|
||||||
optionalDependencies:
|
|
||||||
vite: 5.4.21(@types/node@22.19.15)(sass@1.98.0)(terser@5.46.0)
|
|
||||||
|
|
||||||
'@vitest/mocker@3.2.4(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))':
|
'@vitest/mocker@3.2.4(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/spy': 3.2.4
|
'@vitest/spy': 3.2.4
|
||||||
|
|
@ -18173,6 +18222,15 @@ snapshots:
|
||||||
|
|
||||||
fraction.js@5.3.4: {}
|
fraction.js@5.3.4: {}
|
||||||
|
|
||||||
|
framer-motion@12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||||
|
dependencies:
|
||||||
|
motion-dom: 12.38.0
|
||||||
|
motion-utils: 12.36.0
|
||||||
|
tslib: 2.8.1
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.2.4
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
|
||||||
fresh@0.5.2: {}
|
fresh@0.5.2: {}
|
||||||
|
|
||||||
fresh@2.0.0: {}
|
fresh@2.0.0: {}
|
||||||
|
|
@ -19882,6 +19940,20 @@ snapshots:
|
||||||
|
|
||||||
module-details-from-path@1.0.4: {}
|
module-details-from-path@1.0.4: {}
|
||||||
|
|
||||||
|
motion-dom@12.38.0:
|
||||||
|
dependencies:
|
||||||
|
motion-utils: 12.36.0
|
||||||
|
|
||||||
|
motion-utils@12.36.0: {}
|
||||||
|
|
||||||
|
motion@12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||||
|
dependencies:
|
||||||
|
framer-motion: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
tslib: 2.8.1
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.2.4
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
|
||||||
mrmime@2.0.1: {}
|
mrmime@2.0.1: {}
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
@ -21137,6 +21209,14 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
react-modal-sheet@5.6.0(motion@12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||||
|
dependencies:
|
||||||
|
motion: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
react: 19.2.4
|
||||||
|
react-use-measure: 2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- react-dom
|
||||||
|
|
||||||
react-refresh@0.17.0: {}
|
react-refresh@0.17.0: {}
|
||||||
|
|
||||||
react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4):
|
react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4):
|
||||||
|
|
@ -21173,6 +21253,12 @@ snapshots:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.14
|
'@types/react': 19.2.14
|
||||||
|
|
||||||
|
react-use-measure@2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||||
|
dependencies:
|
||||||
|
react: 19.2.4
|
||||||
|
optionalDependencies:
|
||||||
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
|
||||||
react@18.3.1:
|
react@18.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
|
|
@ -22817,7 +22903,7 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/chai': 5.2.3
|
'@types/chai': 5.2.3
|
||||||
'@vitest/expect': 3.2.4
|
'@vitest/expect': 3.2.4
|
||||||
'@vitest/mocker': 3.2.4(vite@5.4.21(@types/node@22.19.15)(sass@1.98.0)(terser@5.46.0))
|
'@vitest/mocker': 3.2.4(vite@5.4.21(@types/node@25.0.7)(sass@1.98.0)(terser@5.46.0))
|
||||||
'@vitest/pretty-format': 3.2.4
|
'@vitest/pretty-format': 3.2.4
|
||||||
'@vitest/runner': 3.2.4
|
'@vitest/runner': 3.2.4
|
||||||
'@vitest/snapshot': 3.2.4
|
'@vitest/snapshot': 3.2.4
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,15 @@ export const TeamTabSectionNav = ({
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
const [menuPos, setMenuPos] = useState({ top: 0, left: 0, width: 0 });
|
const [menuPos, setMenuPos] = useState({ top: 0, left: 0, width: 0 });
|
||||||
const visibleSections = SECTIONS.filter(
|
const visibleSections = SECTIONS.filter((section) => {
|
||||||
(section) =>
|
if (messagesPanelMode === 'sidebar') {
|
||||||
messagesPanelMode !== 'sidebar' || (section.id !== 'messages' && section.id !== 'claude-logs')
|
return section.id !== 'messages' && section.id !== 'claude-logs';
|
||||||
);
|
}
|
||||||
|
if (messagesPanelMode === 'bottom-sheet') {
|
||||||
|
return section.id !== 'messages';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
const handleNavigate = useCallback(
|
const handleNavigate = useCallback(
|
||||||
(sectionId: string) => {
|
(sectionId: string) => {
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -41,6 +41,7 @@ import type {
|
||||||
interface MessageComposerProps {
|
interface MessageComposerProps {
|
||||||
teamName: string;
|
teamName: string;
|
||||||
members: ResolvedTeamMember[];
|
members: ResolvedTeamMember[];
|
||||||
|
layout?: 'default' | 'compact';
|
||||||
isTeamAlive?: boolean;
|
isTeamAlive?: boolean;
|
||||||
sending: boolean;
|
sending: boolean;
|
||||||
sendError: string | null;
|
sendError: string | null;
|
||||||
|
|
@ -67,6 +68,7 @@ interface MessageComposerProps {
|
||||||
export const MessageComposer = ({
|
export const MessageComposer = ({
|
||||||
teamName,
|
teamName,
|
||||||
members,
|
members,
|
||||||
|
layout = 'default',
|
||||||
isTeamAlive,
|
isTeamAlive,
|
||||||
sending,
|
sending,
|
||||||
sendError,
|
sendError,
|
||||||
|
|
@ -443,10 +445,27 @@ export const MessageComposer = ({
|
||||||
const remaining = MAX_TEXT_LENGTH - trimmed.length;
|
const remaining = MAX_TEXT_LENGTH - trimmed.length;
|
||||||
const hasAttachmentPreviewContent =
|
const hasAttachmentPreviewContent =
|
||||||
draft.attachments.length > 0 || Boolean(draft.attachmentError ?? fileRestrictionError);
|
draft.attachments.length > 0 || Boolean(draft.attachmentError ?? fileRestrictionError);
|
||||||
|
const isCompactLayout = layout === 'compact';
|
||||||
|
const compactFooterNotice = slashCommandRestrictionReason ? (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded bg-amber-500/10 px-1.5 py-0.5 text-[10px] text-amber-300">
|
||||||
|
<AlertCircle size={10} className="shrink-0" />
|
||||||
|
{slashCommandRestrictionReason}
|
||||||
|
</span>
|
||||||
|
) : sendError ? (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded bg-red-500/10 px-1.5 py-0.5 text-[10px] text-red-400">
|
||||||
|
<AlertCircle size={10} className="shrink-0" />
|
||||||
|
{sendError}
|
||||||
|
</span>
|
||||||
|
) : lastResult?.deduplicated ? (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded bg-amber-500/10 px-1.5 py-0.5 text-[10px] text-amber-300">
|
||||||
|
<Check size={10} className="shrink-0" />
|
||||||
|
Reused recent cross-team request
|
||||||
|
</span>
|
||||||
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="relative mb-3 pb-3"
|
className={cn('relative', isCompactLayout ? 'pb-1' : 'mb-3 pb-3')}
|
||||||
role="group"
|
role="group"
|
||||||
onDragEnter={handleDragEnter}
|
onDragEnter={handleDragEnter}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
|
|
@ -454,7 +473,7 @@ export const MessageComposer = ({
|
||||||
onDrop={handleDropWrapper}
|
onDrop={handleDropWrapper}
|
||||||
onPaste={handlePasteWrapper}
|
onPaste={handlePasteWrapper}
|
||||||
>
|
>
|
||||||
<div className="mb-1 space-y-2">
|
<div className={cn('mb-1', isCompactLayout ? 'space-y-1.5' : 'space-y-2')}>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{isLeadRecipient ? (
|
{isLeadRecipient ? (
|
||||||
<>
|
<>
|
||||||
|
|
@ -807,11 +826,17 @@ export const MessageComposer = ({
|
||||||
onShiftTab={handleCycleActionMode}
|
onShiftTab={handleCycleActionMode}
|
||||||
dismissMentionsRef={dismissMentionsRef}
|
dismissMentionsRef={dismissMentionsRef}
|
||||||
extraTips={['Tip: You can use "/" to run any Claude commands.']}
|
extraTips={['Tip: You can use "/" to run any Claude commands.']}
|
||||||
minRows={2}
|
surfaceClassName="message-composer-shell message-composer-orbit-surface border border-transparent bg-[var(--color-surface-raised)] shadow-[0_8px_24px_rgba(0,0,0,0.18),inset_0_1px_0_rgba(255,255,255,0.03)]"
|
||||||
|
surfaceDecoration="orbit-border"
|
||||||
|
surfaceFadeColor="var(--color-surface-raised)"
|
||||||
|
className="border-transparent shadow-none"
|
||||||
|
minRows={isCompactLayout ? 1 : 2}
|
||||||
maxRows={6}
|
maxRows={6}
|
||||||
maxLength={MAX_TEXT_LENGTH}
|
maxLength={MAX_TEXT_LENGTH}
|
||||||
disabled={sending}
|
disabled={sending}
|
||||||
hintText={crossTeamHintText}
|
hintText={crossTeamHintText}
|
||||||
|
showHint={!isCompactLayout}
|
||||||
|
cornerActionInset={isCompactLayout ? 'compact' : 'default'}
|
||||||
cornerActionLeft={
|
cornerActionLeft={
|
||||||
<ActionModeSelector
|
<ActionModeSelector
|
||||||
value={actionMode}
|
value={actionMode}
|
||||||
|
|
@ -859,34 +884,23 @@ export const MessageComposer = ({
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
footerRight={
|
footerRight={
|
||||||
<div className="flex items-center gap-2">
|
isCompactLayout ? (
|
||||||
{slashCommandRestrictionReason ? (
|
compactFooterNotice
|
||||||
<span className="inline-flex items-center gap-1 rounded bg-amber-500/10 px-1.5 py-0.5 text-[10px] text-amber-300">
|
) : (
|
||||||
<AlertCircle size={10} className="shrink-0" />
|
<div className="flex items-center gap-2">
|
||||||
{slashCommandRestrictionReason}
|
{compactFooterNotice}
|
||||||
</span>
|
{remaining < 200 ? (
|
||||||
) : sendError ? (
|
<span
|
||||||
<span className="inline-flex items-center gap-1 rounded bg-red-500/10 px-1.5 py-0.5 text-[10px] text-red-400">
|
className={`text-[10px] ${remaining < 100 ? 'text-yellow-400' : 'text-[var(--color-text-muted)]'}`}
|
||||||
<AlertCircle size={10} className="shrink-0" />
|
>
|
||||||
{sendError}
|
{remaining} chars left
|
||||||
</span>
|
</span>
|
||||||
) : lastResult?.deduplicated ? (
|
) : null}
|
||||||
<span className="inline-flex items-center gap-1 rounded bg-amber-500/10 px-1.5 py-0.5 text-[10px] text-amber-300">
|
{draft.isSaved ? (
|
||||||
<Check size={10} className="shrink-0" />
|
<span className="text-[10px] text-[var(--color-text-muted)]">Saved</span>
|
||||||
Reused recent cross-team request
|
) : null}
|
||||||
</span>
|
</div>
|
||||||
) : null}
|
)
|
||||||
{remaining < 200 ? (
|
|
||||||
<span
|
|
||||||
className={`text-[10px] ${remaining < 100 ? 'text-yellow-400' : 'text-[var(--color-text-muted)]'}`}
|
|
||||||
>
|
|
||||||
{remaining} chars left
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
{draft.isSaved ? (
|
|
||||||
<span className="text-[10px] text-[var(--color-text-muted)]">Saved</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { Sheet, type SheetRef } from 'react-modal-sheet';
|
||||||
|
|
||||||
import { api } from '@renderer/api';
|
import { api } from '@renderer/api';
|
||||||
import { Badge } from '@renderer/components/ui/badge';
|
import { Badge } from '@renderer/components/ui/badge';
|
||||||
|
|
@ -9,21 +10,24 @@ import { useTeamMessagesExpanded } from '@renderer/hooks/useTeamMessagesExpanded
|
||||||
import { useTeamMessagesRead } from '@renderer/hooks/useTeamMessagesRead';
|
import { useTeamMessagesRead } from '@renderer/hooks/useTeamMessagesRead';
|
||||||
import { useStore } from '@renderer/store';
|
import { useStore } from '@renderer/store';
|
||||||
import { mergeTeamMessages } from '@renderer/utils/mergeTeamMessages';
|
import { mergeTeamMessages } from '@renderer/utils/mergeTeamMessages';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
|
||||||
import { filterTeamMessages } from '@renderer/utils/teamMessageFiltering';
|
import { filterTeamMessages } from '@renderer/utils/teamMessageFiltering';
|
||||||
import { toMessageKey } from '@renderer/utils/teamMessageKey';
|
import { toMessageKey } from '@renderer/utils/teamMessageKey';
|
||||||
import { createLogger } from '@shared/utils/logger';
|
|
||||||
import { shouldExcludeInboxTextFromReplyCandidates } from '@shared/utils/idleNotificationSemantics';
|
import { shouldExcludeInboxTextFromReplyCandidates } from '@shared/utils/idleNotificationSemantics';
|
||||||
|
import { createLogger } from '@shared/utils/logger';
|
||||||
import {
|
import {
|
||||||
CheckCheck,
|
CheckCheck,
|
||||||
ChevronsDownUp,
|
ChevronsDownUp,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
|
PanelBottom,
|
||||||
|
PanelBottomClose,
|
||||||
|
PanelBottomOpen,
|
||||||
PanelLeft,
|
PanelLeft,
|
||||||
PanelLeftClose,
|
PanelLeftClose,
|
||||||
Search,
|
Search,
|
||||||
X,
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
import { ActivityTimeline } from '../activity/ActivityTimeline';
|
import { ActivityTimeline } from '../activity/ActivityTimeline';
|
||||||
import { getThoughtGroupKey, groupTimelineItems } from '../activity/LeadThoughtsGroup';
|
import { getThoughtGroupKey, groupTimelineItems } from '../activity/LeadThoughtsGroup';
|
||||||
|
|
@ -41,6 +45,7 @@ import { StatusBlock } from './StatusBlock';
|
||||||
import type { TimelineItem } from '../activity/LeadThoughtsGroup';
|
import type { TimelineItem } from '../activity/LeadThoughtsGroup';
|
||||||
import type { ActionMode } from './ActionModeSelector';
|
import type { ActionMode } from './ActionModeSelector';
|
||||||
import type { MessagesFilterState } from './MessagesFilterPopover';
|
import type { MessagesFilterState } from './MessagesFilterPopover';
|
||||||
|
import type { TeamMessagesPanelMode } from '@renderer/types/teamMessagesPanelMode';
|
||||||
import type { InboxMessage, ResolvedTeamMember, TaskRef, TeamTaskWithKanban } from '@shared/types';
|
import type { InboxMessage, ResolvedTeamMember, TaskRef, TeamTaskWithKanban } from '@shared/types';
|
||||||
|
|
||||||
interface TimeWindow {
|
interface TimeWindow {
|
||||||
|
|
@ -51,11 +56,16 @@ interface TimeWindow {
|
||||||
const logger = createLogger('Component:MessagesPanel');
|
const logger = createLogger('Component:MessagesPanel');
|
||||||
const MESSAGES_PANEL_FILTER_WARN_MS = 8;
|
const MESSAGES_PANEL_FILTER_WARN_MS = 8;
|
||||||
const MESSAGES_PANEL_EXPANDED_ITEM_WARN_MS = 6;
|
const MESSAGES_PANEL_EXPANDED_ITEM_WARN_MS = 6;
|
||||||
|
const BOTTOM_SHEET_HEADER_HEIGHT = 40;
|
||||||
|
const BOTTOM_SHEET_COLLAPSED_SNAP_INDEX = 1;
|
||||||
|
const BOTTOM_SHEET_COMPOSER_SNAP_INDEX = 2;
|
||||||
|
const BOTTOM_SHEET_FULL_SNAP_INDEX = 4;
|
||||||
|
|
||||||
interface MessagesPanelProps {
|
interface MessagesPanelProps {
|
||||||
teamName: string;
|
teamName: string;
|
||||||
position: 'sidebar' | 'inline';
|
position: TeamMessagesPanelMode;
|
||||||
onTogglePosition: () => void;
|
onPositionChange: (position: TeamMessagesPanelMode) => void;
|
||||||
|
mountPoint?: Element | null;
|
||||||
/** Active (non-removed) members. */
|
/** Active (non-removed) members. */
|
||||||
members: ResolvedTeamMember[];
|
members: ResolvedTeamMember[];
|
||||||
/** All team tasks. */
|
/** All team tasks. */
|
||||||
|
|
@ -95,7 +105,8 @@ interface MessagesPanelProps {
|
||||||
export const MessagesPanel = memo(function MessagesPanel({
|
export const MessagesPanel = memo(function MessagesPanel({
|
||||||
teamName,
|
teamName,
|
||||||
position,
|
position,
|
||||||
onTogglePosition,
|
onPositionChange,
|
||||||
|
mountPoint,
|
||||||
members,
|
members,
|
||||||
tasks,
|
tasks,
|
||||||
messages,
|
messages,
|
||||||
|
|
@ -207,6 +218,8 @@ export const MessagesPanel = memo(function MessagesPanel({
|
||||||
|
|
||||||
const composerTextareaRef = useRef<HTMLTextAreaElement | null>(null);
|
const composerTextareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||||
const sidebarScrollRef = useRef<HTMLDivElement | null>(null);
|
const sidebarScrollRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const bottomSheetRef = useRef<SheetRef>(null);
|
||||||
|
const bottomSheetStickyTopRef = useRef<HTMLDivElement | null>(null);
|
||||||
const handleExpandContent = useCallback(() => {
|
const handleExpandContent = useCallback(() => {
|
||||||
// no-op: user is reading expanded content, not composing
|
// no-op: user is reading expanded content, not composing
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -224,15 +237,20 @@ export const MessagesPanel = memo(function MessagesPanel({
|
||||||
const [messagesCollapsed, setMessagesCollapsed] = useState(
|
const [messagesCollapsed, setMessagesCollapsed] = useState(
|
||||||
initialSidebarStateRef.current.messagesCollapsed
|
initialSidebarStateRef.current.messagesCollapsed
|
||||||
);
|
);
|
||||||
const [sidebarSearchVisible, setSidebarSearchVisible] = useState(
|
const [messagesSearchBarVisible, setMessagesSearchBarVisible] = useState(
|
||||||
initialSidebarStateRef.current.sidebarSearchVisible
|
initialSidebarStateRef.current.messagesSearchBarVisible
|
||||||
);
|
);
|
||||||
const [expandedItemKey, setExpandedItemKey] = useState<string | null>(
|
const [expandedItemKey, setExpandedItemKey] = useState<string | null>(
|
||||||
initialSidebarStateRef.current.expandedItemKey
|
initialSidebarStateRef.current.expandedItemKey
|
||||||
);
|
);
|
||||||
const [sidebarScrollTop, setSidebarScrollTop] = useState(
|
const [messagesScrollTop, setMessagesScrollTop] = useState(
|
||||||
initialSidebarStateRef.current.sidebarScrollTop
|
initialSidebarStateRef.current.messagesScrollTop
|
||||||
);
|
);
|
||||||
|
const [bottomSheetSnapIndex, setBottomSheetSnapIndex] = useState(
|
||||||
|
initialSidebarStateRef.current.bottomSheetSnapIndex
|
||||||
|
);
|
||||||
|
const [bottomSheetStickyTopHeight, setBottomSheetStickyTopHeight] = useState(196);
|
||||||
|
const [bottomSheetMountHeight, setBottomSheetMountHeight] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initialSidebarStateRef.current = getTeamMessagesSidebarUiState(teamName);
|
initialSidebarStateRef.current = getTeamMessagesSidebarUiState(teamName);
|
||||||
|
|
@ -240,9 +258,10 @@ export const MessagesPanel = memo(function MessagesPanel({
|
||||||
setMessagesFilter(initialSidebarStateRef.current.messagesFilter);
|
setMessagesFilter(initialSidebarStateRef.current.messagesFilter);
|
||||||
setMessagesFilterOpen(initialSidebarStateRef.current.messagesFilterOpen);
|
setMessagesFilterOpen(initialSidebarStateRef.current.messagesFilterOpen);
|
||||||
setMessagesCollapsed(initialSidebarStateRef.current.messagesCollapsed);
|
setMessagesCollapsed(initialSidebarStateRef.current.messagesCollapsed);
|
||||||
setSidebarSearchVisible(initialSidebarStateRef.current.sidebarSearchVisible);
|
setMessagesSearchBarVisible(initialSidebarStateRef.current.messagesSearchBarVisible);
|
||||||
setExpandedItemKey(initialSidebarStateRef.current.expandedItemKey);
|
setExpandedItemKey(initialSidebarStateRef.current.expandedItemKey);
|
||||||
setSidebarScrollTop(initialSidebarStateRef.current.sidebarScrollTop);
|
setMessagesScrollTop(initialSidebarStateRef.current.messagesScrollTop);
|
||||||
|
setBottomSheetSnapIndex(initialSidebarStateRef.current.bottomSheetSnapIndex);
|
||||||
}, [teamName]);
|
}, [teamName]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -251,9 +270,10 @@ export const MessagesPanel = memo(function MessagesPanel({
|
||||||
messagesFilter,
|
messagesFilter,
|
||||||
messagesFilterOpen,
|
messagesFilterOpen,
|
||||||
messagesCollapsed,
|
messagesCollapsed,
|
||||||
sidebarSearchVisible,
|
messagesSearchBarVisible,
|
||||||
expandedItemKey,
|
expandedItemKey,
|
||||||
sidebarScrollTop,
|
messagesScrollTop,
|
||||||
|
bottomSheetSnapIndex,
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
teamName,
|
teamName,
|
||||||
|
|
@ -261,17 +281,52 @@ export const MessagesPanel = memo(function MessagesPanel({
|
||||||
messagesFilter,
|
messagesFilter,
|
||||||
messagesFilterOpen,
|
messagesFilterOpen,
|
||||||
messagesCollapsed,
|
messagesCollapsed,
|
||||||
sidebarSearchVisible,
|
messagesSearchBarVisible,
|
||||||
expandedItemKey,
|
expandedItemKey,
|
||||||
sidebarScrollTop,
|
messagesScrollTop,
|
||||||
|
bottomSheetSnapIndex,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (position !== 'sidebar') return;
|
if (position !== 'sidebar') return;
|
||||||
const el = sidebarScrollRef.current;
|
const el = sidebarScrollRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
el.scrollTop = sidebarScrollTop;
|
el.scrollTop = messagesScrollTop;
|
||||||
}, [position, sidebarScrollTop]);
|
}, [position, messagesScrollTop]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (position !== 'bottom-sheet' || typeof ResizeObserver === 'undefined') return;
|
||||||
|
|
||||||
|
const mountPointElement = mountPoint instanceof HTMLElement ? mountPoint : null;
|
||||||
|
const observedEntries: Array<[Element | null, (height: number) => void]> = [
|
||||||
|
[bottomSheetStickyTopRef.current, setBottomSheetStickyTopHeight],
|
||||||
|
[mountPointElement, setBottomSheetMountHeight],
|
||||||
|
];
|
||||||
|
const observers: ResizeObserver[] = [];
|
||||||
|
|
||||||
|
for (const [element, setHeight] of observedEntries) {
|
||||||
|
if (!element) continue;
|
||||||
|
|
||||||
|
const updateHeight = () => {
|
||||||
|
const nextHeight = Math.ceil(element.getBoundingClientRect().height);
|
||||||
|
if (nextHeight > 0) {
|
||||||
|
setHeight(nextHeight);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateHeight();
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(() => {
|
||||||
|
updateHeight();
|
||||||
|
});
|
||||||
|
observer.observe(element);
|
||||||
|
observers.push(observer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observers.forEach((observer) => observer.disconnect());
|
||||||
|
};
|
||||||
|
}, [position, mountPoint]);
|
||||||
|
|
||||||
const filteredMessages = useMemo(() => {
|
const filteredMessages = useMemo(() => {
|
||||||
const startedAt = performance.now();
|
const startedAt = performance.now();
|
||||||
|
|
@ -348,7 +403,7 @@ export const MessagesPanel = memo(function MessagesPanel({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}, [expandedItemKey, activityTimelineMessages]);
|
}, [expandedItemKey, activityTimelineMessages, teamName]);
|
||||||
|
|
||||||
// Auto-clear stale expanded key
|
// Auto-clear stale expanded key
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -461,6 +516,60 @@ export const MessagesPanel = memo(function MessagesPanel({
|
||||||
[teamName, sendCrossTeamMessage]
|
[teamName, sendCrossTeamMessage]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const moveToInline = useCallback(() => {
|
||||||
|
onPositionChange('inline');
|
||||||
|
}, [onPositionChange]);
|
||||||
|
|
||||||
|
const moveToSidebar = useCallback(() => {
|
||||||
|
onPositionChange('sidebar');
|
||||||
|
}, [onPositionChange]);
|
||||||
|
|
||||||
|
const moveToBottomSheet = useCallback(() => {
|
||||||
|
setBottomSheetSnapIndex(BOTTOM_SHEET_COMPOSER_SNAP_INDEX);
|
||||||
|
onPositionChange('bottom-sheet');
|
||||||
|
}, [onPositionChange]);
|
||||||
|
|
||||||
|
const snapBottomSheetTo = useCallback((snapIndex: number) => {
|
||||||
|
setBottomSheetSnapIndex(snapIndex);
|
||||||
|
bottomSheetRef.current?.snapTo(snapIndex);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleBottomSheetExpansion = useCallback(() => {
|
||||||
|
if (bottomSheetSnapIndex === BOTTOM_SHEET_COLLAPSED_SNAP_INDEX) {
|
||||||
|
snapBottomSheetTo(BOTTOM_SHEET_COMPOSER_SNAP_INDEX);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
snapBottomSheetTo(BOTTOM_SHEET_COLLAPSED_SNAP_INDEX);
|
||||||
|
}, [bottomSheetSnapIndex, snapBottomSheetTo]);
|
||||||
|
|
||||||
|
const bottomSheetSnapPoints = useMemo(() => {
|
||||||
|
const maxOpenHeight =
|
||||||
|
bottomSheetMountHeight > 0
|
||||||
|
? Math.max(bottomSheetMountHeight - 1, 96)
|
||||||
|
: Number.POSITIVE_INFINITY;
|
||||||
|
const collapsedHeight = Math.min(BOTTOM_SHEET_HEADER_HEIGHT, maxOpenHeight);
|
||||||
|
const composerHeight = Math.min(
|
||||||
|
Math.max(collapsedHeight + bottomSheetStickyTopHeight, collapsedHeight + 120),
|
||||||
|
maxOpenHeight
|
||||||
|
);
|
||||||
|
const centeredHeight = Math.min(
|
||||||
|
Math.max(
|
||||||
|
bottomSheetMountHeight > 0 ? Math.round(bottomSheetMountHeight * 0.58) : 520,
|
||||||
|
composerHeight + 140
|
||||||
|
),
|
||||||
|
maxOpenHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
return [0, collapsedHeight, composerHeight, centeredHeight, 1];
|
||||||
|
}, [bottomSheetMountHeight, bottomSheetStickyTopHeight]);
|
||||||
|
|
||||||
|
const normalizedBottomSheetSnapIndex = useMemo(() => {
|
||||||
|
return Math.min(
|
||||||
|
Math.max(bottomSheetSnapIndex, BOTTOM_SHEET_COLLAPSED_SNAP_INDEX),
|
||||||
|
BOTTOM_SHEET_FULL_SNAP_INDEX
|
||||||
|
);
|
||||||
|
}, [bottomSheetSnapIndex]);
|
||||||
|
|
||||||
// ---- Shared content (used in both modes) ----
|
// ---- Shared content (used in both modes) ----
|
||||||
const searchAndFilterControls = (
|
const searchAndFilterControls = (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
@ -602,9 +711,9 @@ export const MessagesPanel = memo(function MessagesPanel({
|
||||||
// ---- Sidebar mode ----
|
// ---- Sidebar mode ----
|
||||||
if (position === 'sidebar') {
|
if (position === 'sidebar') {
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full flex-col overflow-hidden bg-[var(--color-surface)]">
|
<div className="flex size-full flex-col overflow-hidden bg-[var(--color-surface-sidebar)]">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex shrink-0 items-center gap-2 border-b border-[var(--color-border)] bg-[var(--color-section-bg)] px-3 py-2">
|
<div className="flex shrink-0 items-center gap-2 border-b border-[var(--color-border)] bg-[var(--color-surface-sidebar)] px-3 py-2">
|
||||||
<MessageSquare size={14} className="shrink-0 text-[var(--color-text-muted)]" />
|
<MessageSquare size={14} className="shrink-0 text-[var(--color-text-muted)]" />
|
||||||
<span className="text-sm font-medium text-[var(--color-text)]">Messages</span>
|
<span className="text-sm font-medium text-[var(--color-text)]">Messages</span>
|
||||||
{filteredMessages.length > 0 && (
|
{filteredMessages.length > 0 && (
|
||||||
|
|
@ -650,6 +759,7 @@ export const MessagesPanel = memo(function MessagesPanel({
|
||||||
size="sm"
|
size="sm"
|
||||||
className="size-7 p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
className="size-7 p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
||||||
onClick={() => setMessagesCollapsed((v) => !v)}
|
onClick={() => setMessagesCollapsed((v) => !v)}
|
||||||
|
aria-label={messagesCollapsed ? 'Expand all messages' : 'Collapse all messages'}
|
||||||
>
|
>
|
||||||
{messagesCollapsed ? <ChevronsUpDown size={14} /> : <ChevronsDownUp size={14} />}
|
{messagesCollapsed ? <ChevronsUpDown size={14} /> : <ChevronsDownUp size={14} />}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -664,13 +774,16 @@ export const MessagesPanel = memo(function MessagesPanel({
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="size-7 p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
className="size-7 p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
||||||
onClick={() => setSidebarSearchVisible((v) => !v)}
|
onClick={() => setMessagesSearchBarVisible((v) => !v)}
|
||||||
|
aria-label={
|
||||||
|
messagesSearchBarVisible ? 'Hide message search' : 'Show message search'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{sidebarSearchVisible ? <X size={14} /> : <Search size={14} />}
|
{messagesSearchBarVisible ? <X size={14} /> : <Search size={14} />}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="bottom">
|
<TooltipContent side="bottom">
|
||||||
{sidebarSearchVisible ? 'Hide search' : 'Search messages'}
|
{messagesSearchBarVisible ? 'Hide search' : 'Search messages'}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
|
@ -679,7 +792,8 @@ export const MessagesPanel = memo(function MessagesPanel({
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="size-7 p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
className="size-7 p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
||||||
onClick={onTogglePosition}
|
onClick={moveToInline}
|
||||||
|
aria-label="Move messages to inline panel"
|
||||||
>
|
>
|
||||||
<PanelLeftClose size={14} />
|
<PanelLeftClose size={14} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -689,7 +803,7 @@ export const MessagesPanel = memo(function MessagesPanel({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Search & filter bar (toggleable) */}
|
{/* Search & filter bar (toggleable) */}
|
||||||
{sidebarSearchVisible && (
|
{messagesSearchBarVisible && (
|
||||||
<div className="shrink-0 border-b border-[var(--color-border)] px-3 py-1.5">
|
<div className="shrink-0 border-b border-[var(--color-border)] px-3 py-1.5">
|
||||||
{searchAndFilterControls}
|
{searchAndFilterControls}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -698,7 +812,7 @@ export const MessagesPanel = memo(function MessagesPanel({
|
||||||
<div
|
<div
|
||||||
ref={sidebarScrollRef}
|
ref={sidebarScrollRef}
|
||||||
className="min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden pb-14 pr-3 pt-2"
|
className="min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden pb-14 pr-3 pt-2"
|
||||||
onScroll={(e) => setSidebarScrollTop(e.currentTarget.scrollTop)}
|
onScroll={(e) => setMessagesScrollTop(e.currentTarget.scrollTop)}
|
||||||
>
|
>
|
||||||
<div className="pl-3">
|
<div className="pl-3">
|
||||||
<MessageComposer
|
<MessageComposer
|
||||||
|
|
@ -780,6 +894,295 @@ export const MessagesPanel = memo(function MessagesPanel({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (position === 'bottom-sheet') {
|
||||||
|
if (!mountPoint) {
|
||||||
|
return <div className="hidden" aria-hidden="true" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isBottomSheetCollapsed =
|
||||||
|
normalizedBottomSheetSnapIndex === BOTTOM_SHEET_COLLAPSED_SNAP_INDEX;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet
|
||||||
|
ref={bottomSheetRef}
|
||||||
|
isOpen
|
||||||
|
onClose={moveToInline}
|
||||||
|
mountPoint={mountPoint}
|
||||||
|
avoidKeyboard={false}
|
||||||
|
detent="full"
|
||||||
|
snapPoints={bottomSheetSnapPoints}
|
||||||
|
initialSnap={normalizedBottomSheetSnapIndex}
|
||||||
|
onSnap={setBottomSheetSnapIndex}
|
||||||
|
disableDismiss
|
||||||
|
disableScrollLocking
|
||||||
|
style={{ zIndex: 30 }}
|
||||||
|
className="!pointer-events-none !absolute !inset-0"
|
||||||
|
unstyled
|
||||||
|
>
|
||||||
|
<Sheet.Container
|
||||||
|
unstyled
|
||||||
|
className="flex max-h-full w-full flex-col overflow-hidden rounded-t-[20px] border border-[var(--color-border)] bg-[var(--color-surface-sidebar)] shadow-[0_-18px_48px_rgba(0,0,0,0.35)]"
|
||||||
|
>
|
||||||
|
<Sheet.Header
|
||||||
|
unstyled
|
||||||
|
className="shrink-0 cursor-grab select-none border-b border-[var(--color-border)] bg-[var(--color-surface-sidebar)] active:cursor-grabbing"
|
||||||
|
>
|
||||||
|
<div className="relative h-10 px-3">
|
||||||
|
<div className="pointer-events-none absolute inset-x-0 top-1 flex justify-center">
|
||||||
|
<Sheet.DragIndicator
|
||||||
|
className="!h-1 !w-9 cursor-grab !rounded-full active:cursor-grabbing"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'color-mix(in srgb, var(--color-text-muted) 45%, transparent)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex h-full items-center gap-1.5">
|
||||||
|
<MessageSquare size={13} className="shrink-0 text-[var(--color-text-muted)]" />
|
||||||
|
<span className="text-[13px] font-medium text-[var(--color-text)]">Messages</span>
|
||||||
|
{filteredMessages.length > 0 && (
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="px-1 py-0 text-[9px] font-normal leading-none"
|
||||||
|
>
|
||||||
|
{filteredMessages.length}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{messagesUnreadCount > 0 && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="bg-blue-500/20 px-1 py-0 text-[9px] font-normal leading-none text-blue-600 dark:text-blue-400"
|
||||||
|
>
|
||||||
|
{messagesUnreadCount} new
|
||||||
|
</Badge>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">{messagesUnreadCount} unread</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className="ml-auto flex items-center gap-1"
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{messagesUnreadCount > 0 && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="size-[22px] p-0 text-blue-400 hover:bg-blue-500/10 hover:text-blue-300"
|
||||||
|
onClick={handleMarkAllRead}
|
||||||
|
aria-label="Mark all messages as read"
|
||||||
|
>
|
||||||
|
<CheckCheck size={13} />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">Mark all as read</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="size-[22px] p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
||||||
|
onClick={() => setMessagesCollapsed((value) => !value)}
|
||||||
|
aria-label={
|
||||||
|
messagesCollapsed ? 'Expand all messages' : 'Collapse all messages'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{messagesCollapsed ? (
|
||||||
|
<ChevronsUpDown size={14} />
|
||||||
|
) : (
|
||||||
|
<ChevronsDownUp size={14} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">
|
||||||
|
{messagesCollapsed ? 'Expand all messages' : 'Collapse all messages'}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="size-[22px] p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
||||||
|
onClick={() => setMessagesSearchBarVisible((value) => !value)}
|
||||||
|
aria-label={
|
||||||
|
messagesSearchBarVisible ? 'Hide message search' : 'Show message search'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{messagesSearchBarVisible ? <X size={14} /> : <Search size={14} />}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">
|
||||||
|
{messagesSearchBarVisible ? 'Hide search' : 'Search messages'}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="size-[22px] p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
||||||
|
onClick={toggleBottomSheetExpansion}
|
||||||
|
aria-label={
|
||||||
|
isBottomSheetCollapsed
|
||||||
|
? 'Expand messages bottom sheet'
|
||||||
|
: 'Collapse messages bottom sheet'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isBottomSheetCollapsed ? (
|
||||||
|
<PanelBottomOpen size={14} />
|
||||||
|
) : (
|
||||||
|
<PanelBottomClose size={14} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">
|
||||||
|
{isBottomSheetCollapsed ? 'Expand sheet' : 'Collapse sheet'}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="size-[22px] p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
||||||
|
onClick={moveToInline}
|
||||||
|
aria-label="Move messages to inline panel"
|
||||||
|
>
|
||||||
|
<PanelBottom size={14} />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">Move to inline</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="size-[22px] p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
||||||
|
onClick={moveToSidebar}
|
||||||
|
aria-label="Move messages to sidebar"
|
||||||
|
>
|
||||||
|
<PanelLeft size={14} />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">Move to sidebar</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Sheet.Header>
|
||||||
|
{!isBottomSheetCollapsed && (
|
||||||
|
<Sheet.Content
|
||||||
|
className="min-h-0 bg-[var(--color-surface-sidebar)]"
|
||||||
|
scrollClassName="flex min-h-full flex-col"
|
||||||
|
disableDrag={(state) => state.scrollPosition !== 'top'}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={bottomSheetStickyTopRef}
|
||||||
|
className="sticky top-0 z-[1] shrink-0 border-b border-[var(--color-border)] backdrop-blur"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--color-surface-sidebar)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{messagesSearchBarVisible && (
|
||||||
|
<div className="border-b border-[var(--color-border)] px-3 py-2">
|
||||||
|
{searchAndFilterControls}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="px-3 pb-3 pt-3">
|
||||||
|
<MessageComposer
|
||||||
|
teamName={teamName}
|
||||||
|
layout="compact"
|
||||||
|
members={members}
|
||||||
|
isTeamAlive={isTeamAlive}
|
||||||
|
sending={sendingMessage}
|
||||||
|
sendError={sendMessageError}
|
||||||
|
lastResult={lastSendMessageResult}
|
||||||
|
textareaRef={composerTextareaRef}
|
||||||
|
onSend={handleSend}
|
||||||
|
onCrossTeamSend={handleCrossTeamSend}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="shrink-0 px-3 pt-2">
|
||||||
|
<StatusBlock
|
||||||
|
members={members}
|
||||||
|
tasks={tasks}
|
||||||
|
messages={effectiveMessages}
|
||||||
|
pendingRepliesByMember={pendingRepliesByMember}
|
||||||
|
layout="flow"
|
||||||
|
position="inline"
|
||||||
|
onMemberClick={onMemberClick}
|
||||||
|
onTaskClick={onTaskClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 px-3 pb-4 pt-2">
|
||||||
|
<ActivityTimeline
|
||||||
|
messages={activityTimelineMessages}
|
||||||
|
teamName={teamName}
|
||||||
|
members={members}
|
||||||
|
readState={readState}
|
||||||
|
allCollapsed={messagesCollapsed}
|
||||||
|
expandOverrides={expandedSet}
|
||||||
|
onToggleExpandOverride={toggleExpandOverride}
|
||||||
|
teamSessionIds={teamSessionIds}
|
||||||
|
currentLeadSessionId={currentLeadSessionId}
|
||||||
|
isTeamAlive={isTeamAlive}
|
||||||
|
leadActivity={leadActivity}
|
||||||
|
leadContextUpdatedAt={leadContextUpdatedAt}
|
||||||
|
teamNames={teamNames}
|
||||||
|
teamColorByName={teamColorByName}
|
||||||
|
onTeamClick={openTeamTab}
|
||||||
|
onMemberClick={onMemberClick}
|
||||||
|
onCreateTaskFromMessage={onCreateTaskFromMessage}
|
||||||
|
onReplyToMessage={onReplyToMessage}
|
||||||
|
onMessageVisible={handleMessageVisible}
|
||||||
|
onRestartTeam={onRestartTeam}
|
||||||
|
onTaskIdClick={onTaskIdClick}
|
||||||
|
onExpandItem={handleExpandItem}
|
||||||
|
onExpandContent={handleExpandContent}
|
||||||
|
/>
|
||||||
|
{hasMore && (
|
||||||
|
<div className="flex justify-center py-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-xs text-text-muted"
|
||||||
|
disabled={messagesLoading}
|
||||||
|
onClick={() => void loadOlderMessages()}
|
||||||
|
>
|
||||||
|
{messagesLoading ? 'Loading...' : 'Load older messages'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<MessageExpandDialog
|
||||||
|
expandedItem={expandedItem}
|
||||||
|
open={expandedItemKey !== null}
|
||||||
|
onOpenChange={handleExpandDialogChange}
|
||||||
|
teamName={teamName}
|
||||||
|
members={members}
|
||||||
|
onCreateTaskFromMessage={onCreateTaskFromMessage}
|
||||||
|
onReplyToMessage={onReplyToMessage}
|
||||||
|
onMemberClick={onMemberClick}
|
||||||
|
onTaskIdClick={onTaskIdClick}
|
||||||
|
onRestartTeam={onRestartTeam}
|
||||||
|
teamNames={teamNames}
|
||||||
|
teamColorByName={teamColorByName}
|
||||||
|
onTeamClick={openTeamTab}
|
||||||
|
/>
|
||||||
|
</Sheet.Content>
|
||||||
|
)}
|
||||||
|
</Sheet.Container>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Inline mode (wrapped in CollapsibleTeamSection) ----
|
// ---- Inline mode (wrapped in CollapsibleTeamSection) ----
|
||||||
return (
|
return (
|
||||||
<CollapsibleTeamSection
|
<CollapsibleTeamSection
|
||||||
|
|
@ -810,22 +1213,42 @@ export const MessagesPanel = memo(function MessagesPanel({
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
headerExtra={
|
headerExtra={
|
||||||
<Tooltip>
|
<div className="flex items-center gap-1">
|
||||||
<TooltipTrigger asChild>
|
<Tooltip>
|
||||||
<Button
|
<TooltipTrigger asChild>
|
||||||
variant="ghost"
|
<Button
|
||||||
size="sm"
|
variant="ghost"
|
||||||
className="pointer-events-auto size-6 p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
size="sm"
|
||||||
onClick={(e) => {
|
className="pointer-events-auto size-6 p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
||||||
e.stopPropagation();
|
onClick={(e) => {
|
||||||
onTogglePosition();
|
e.stopPropagation();
|
||||||
}}
|
moveToBottomSheet();
|
||||||
>
|
}}
|
||||||
<PanelLeft size={14} />
|
aria-label="Move messages to bottom sheet"
|
||||||
</Button>
|
>
|
||||||
</TooltipTrigger>
|
<PanelBottom size={14} />
|
||||||
<TooltipContent side="top">Move to sidebar</TooltipContent>
|
</Button>
|
||||||
</Tooltip>
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">Move to bottom sheet</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="pointer-events-auto size-6 p-0 text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
moveToSidebar();
|
||||||
|
}}
|
||||||
|
aria-label="Move messages to sidebar"
|
||||||
|
>
|
||||||
|
<PanelLeft size={14} />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top">Move to sidebar</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
defaultOpen
|
defaultOpen
|
||||||
action={<div className="flex items-center gap-2 px-2">{searchAndFilterBar}</div>}
|
action={<div className="flex items-center gap-2 px-2">{searchAndFilterBar}</div>}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ interface StatusBlockProps {
|
||||||
pendingRepliesByMember: Record<string, number>;
|
pendingRepliesByMember: Record<string, number>;
|
||||||
/** Where the Messages panel is rendered — 'sidebar' hides "In progress" (already visible in MemberList). */
|
/** Where the Messages panel is rendered — 'sidebar' hides "In progress" (already visible in MemberList). */
|
||||||
position?: 'sidebar' | 'inline';
|
position?: 'sidebar' | 'inline';
|
||||||
|
/** Overlay keeps the toggle hovering over the previous section, flow keeps it in normal layout. */
|
||||||
|
layout?: 'overlay' | 'flow';
|
||||||
onMemberClick?: (member: ResolvedTeamMember) => void;
|
onMemberClick?: (member: ResolvedTeamMember) => void;
|
||||||
onTaskClick?: (task: TeamTaskWithKanban) => void;
|
onTaskClick?: (task: TeamTaskWithKanban) => void;
|
||||||
}
|
}
|
||||||
|
|
@ -31,6 +33,7 @@ export const StatusBlock = ({
|
||||||
messages,
|
messages,
|
||||||
pendingRepliesByMember,
|
pendingRepliesByMember,
|
||||||
position,
|
position,
|
||||||
|
layout = 'overlay',
|
||||||
onMemberClick,
|
onMemberClick,
|
||||||
onTaskClick,
|
onTaskClick,
|
||||||
}: StatusBlockProps): React.JSX.Element | null => {
|
}: StatusBlockProps): React.JSX.Element | null => {
|
||||||
|
|
@ -68,24 +71,32 @@ export const StatusBlock = ({
|
||||||
|
|
||||||
if (!hasItems) return null;
|
if (!hasItems) return null;
|
||||||
|
|
||||||
|
const toggleButton = (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-1 text-[10px] font-medium uppercase tracking-wide text-[var(--color-text-muted)] transition-colors hover:text-[var(--color-text-secondary)]"
|
||||||
|
onClick={() => setCollapsed((prev) => !prev)}
|
||||||
|
aria-label={collapsed ? 'Expand status' : 'Collapse status'}
|
||||||
|
>
|
||||||
|
<ChevronRight
|
||||||
|
size={12}
|
||||||
|
className={`shrink-0 transition-transform duration-150 ${collapsed ? '' : 'rotate-90'}`}
|
||||||
|
/>
|
||||||
|
Status
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="relative h-0">
|
{layout === 'overlay' ? (
|
||||||
<button
|
<div className="relative h-0">
|
||||||
type="button"
|
<div className="absolute -top-[19px] right-0 z-10">{toggleButton}</div>
|
||||||
className="absolute -top-[19px] right-0 z-10 flex items-center gap-1 text-[10px] font-medium uppercase tracking-wide text-[var(--color-text-muted)] transition-colors hover:text-[var(--color-text-secondary)]"
|
</div>
|
||||||
onClick={() => setCollapsed((prev) => !prev)}
|
) : (
|
||||||
aria-label={collapsed ? 'Expand status' : 'Collapse status'}
|
<div className="mb-2 flex justify-end">{toggleButton}</div>
|
||||||
>
|
)}
|
||||||
<ChevronRight
|
|
||||||
size={12}
|
|
||||||
className={`shrink-0 transition-transform duration-150 ${collapsed ? '' : 'rotate-90'}`}
|
|
||||||
/>
|
|
||||||
Status
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<div className="mt-5">
|
<div className={layout === 'overlay' ? 'mt-5' : ''}>
|
||||||
<PendingRepliesBlock
|
<PendingRepliesBlock
|
||||||
members={members}
|
members={members}
|
||||||
pendingRepliesByMember={pendingRepliesByMember}
|
pendingRepliesByMember={pendingRepliesByMember}
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,10 @@ export interface TeamMessagesSidebarUiState {
|
||||||
messagesFilter: MessagesFilterState;
|
messagesFilter: MessagesFilterState;
|
||||||
messagesFilterOpen: boolean;
|
messagesFilterOpen: boolean;
|
||||||
messagesCollapsed: boolean;
|
messagesCollapsed: boolean;
|
||||||
sidebarSearchVisible: boolean;
|
messagesSearchBarVisible: boolean;
|
||||||
expandedItemKey: string | null;
|
expandedItemKey: string | null;
|
||||||
sidebarScrollTop: number;
|
messagesScrollTop: number;
|
||||||
|
bottomSheetSnapIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TeamClaudeLogsSidebarUiState {
|
export interface TeamClaudeLogsSidebarUiState {
|
||||||
|
|
@ -58,9 +59,10 @@ export function createDefaultMessagesSidebarUiState(): TeamMessagesSidebarUiStat
|
||||||
},
|
},
|
||||||
messagesFilterOpen: false,
|
messagesFilterOpen: false,
|
||||||
messagesCollapsed: true,
|
messagesCollapsed: true,
|
||||||
sidebarSearchVisible: false,
|
messagesSearchBarVisible: false,
|
||||||
expandedItemKey: null,
|
expandedItemKey: null,
|
||||||
sidebarScrollTop: 0,
|
messagesScrollTop: 0,
|
||||||
|
bottomSheetSnapIndex: 2,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -326,6 +326,12 @@ interface MentionableTextareaProps extends Omit<
|
||||||
value: string;
|
value: string;
|
||||||
onValueChange: (v: string) => void;
|
onValueChange: (v: string) => void;
|
||||||
suggestions: MentionSuggestion[];
|
suggestions: MentionSuggestion[];
|
||||||
|
/** Surface class applied behind the textarea/overlay content. */
|
||||||
|
surfaceClassName?: string;
|
||||||
|
/** Optional decorative treatment for the surface shell. */
|
||||||
|
surfaceDecoration?: 'none' | 'orbit-border';
|
||||||
|
/** Solid color used by the bottom fade behind corner actions. */
|
||||||
|
surfaceFadeColor?: string;
|
||||||
hintText?: string;
|
hintText?: string;
|
||||||
showHint?: boolean;
|
showHint?: boolean;
|
||||||
/** Content rendered at the right side of the footer row (e.g. "Saved") */
|
/** Content rendered at the right side of the footer row (e.g. "Saved") */
|
||||||
|
|
@ -334,6 +340,8 @@ interface MentionableTextareaProps extends Omit<
|
||||||
cornerAction?: React.ReactNode;
|
cornerAction?: React.ReactNode;
|
||||||
/** Content rendered in the bottom-left corner inside the textarea (e.g. mode selector) */
|
/** Content rendered in the bottom-left corner inside the textarea (e.g. mode selector) */
|
||||||
cornerActionLeft?: React.ReactNode;
|
cornerActionLeft?: React.ReactNode;
|
||||||
|
/** Density of the reserved bottom inset used by corner actions. */
|
||||||
|
cornerActionInset?: 'default' | 'compact';
|
||||||
/** Inline code chips to display as badges */
|
/** Inline code chips to display as badges */
|
||||||
chips?: InlineChip[];
|
chips?: InlineChip[];
|
||||||
/** Called when a chip is removed (by X button, backspace, or reconciliation) */
|
/** Called when a chip is removed (by X button, backspace, or reconciliation) */
|
||||||
|
|
@ -364,11 +372,15 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
|
||||||
value,
|
value,
|
||||||
onValueChange,
|
onValueChange,
|
||||||
suggestions,
|
suggestions,
|
||||||
|
surfaceClassName,
|
||||||
|
surfaceDecoration = 'none',
|
||||||
|
surfaceFadeColor = 'var(--color-surface-raised)',
|
||||||
hintText,
|
hintText,
|
||||||
showHint = true,
|
showHint = true,
|
||||||
footerRight,
|
footerRight,
|
||||||
cornerAction,
|
cornerAction,
|
||||||
cornerActionLeft,
|
cornerActionLeft,
|
||||||
|
cornerActionInset = 'default',
|
||||||
chips = [],
|
chips = [],
|
||||||
onChipRemove,
|
onChipRemove,
|
||||||
projectPath,
|
projectPath,
|
||||||
|
|
@ -388,8 +400,15 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
|
||||||
) => {
|
) => {
|
||||||
const internalRef = React.useRef<HTMLTextAreaElement | null>(null);
|
const internalRef = React.useRef<HTMLTextAreaElement | null>(null);
|
||||||
const backdropRef = React.useRef<HTMLDivElement>(null);
|
const backdropRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
const surfaceShellRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
const [scrollTop, setScrollTop] = React.useState(0);
|
const [scrollTop, setScrollTop] = React.useState(0);
|
||||||
const { isLight } = useTheme();
|
const { isLight } = useTheme();
|
||||||
|
const orbitGlowId = React.useId();
|
||||||
|
const [surfaceShellMetrics, setSurfaceShellMetrics] = React.useState(() => ({
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
borderRadius: 6,
|
||||||
|
}));
|
||||||
|
|
||||||
// --- File search activation ---
|
// --- File search activation ---
|
||||||
const enableFiles = !!projectPath;
|
const enableFiles = !!projectPath;
|
||||||
|
|
@ -649,6 +668,39 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
|
||||||
backdrop.style.tabSize = cs.tabSize;
|
backdrop.style.tabSize = cs.tabSize;
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
if (surfaceDecoration !== 'orbit-border') return;
|
||||||
|
const shell = surfaceShellRef.current;
|
||||||
|
if (!shell) return;
|
||||||
|
|
||||||
|
const updateMetrics = () => {
|
||||||
|
const rect = shell.getBoundingClientRect();
|
||||||
|
const computedStyle = window.getComputedStyle(shell);
|
||||||
|
const borderRadius = Number.parseFloat(computedStyle.borderTopLeftRadius) || 6;
|
||||||
|
setSurfaceShellMetrics((prev) => {
|
||||||
|
if (
|
||||||
|
Math.abs(prev.width - rect.width) < 0.5 &&
|
||||||
|
Math.abs(prev.height - rect.height) < 0.5 &&
|
||||||
|
Math.abs(prev.borderRadius - borderRadius) < 0.5
|
||||||
|
) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height,
|
||||||
|
borderRadius,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
updateMetrics();
|
||||||
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
|
updateMetrics();
|
||||||
|
});
|
||||||
|
resizeObserver.observe(shell);
|
||||||
|
return () => resizeObserver.disconnect();
|
||||||
|
}, [surfaceDecoration]);
|
||||||
|
|
||||||
// --- Overlay activation ---
|
// --- Overlay activation ---
|
||||||
const hasOverlay =
|
const hasOverlay =
|
||||||
value.includes('http://') ||
|
value.includes('http://') ||
|
||||||
|
|
@ -1043,17 +1095,99 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
|
||||||
enableTaskSearch ||
|
enableTaskSearch ||
|
||||||
commandSuggestions.length > 0);
|
commandSuggestions.length > 0);
|
||||||
const showFooter = showHintRow || footerRight;
|
const showFooter = showHintRow || footerRight;
|
||||||
|
const hasCornerActions = Boolean(cornerAction || cornerActionLeft);
|
||||||
|
const cornerInsetClass = cornerActionInset === 'compact' ? 'pb-10' : 'pb-12';
|
||||||
|
const cornerFadeHeight = cornerActionInset === 'compact' ? 40 : 48;
|
||||||
|
const cornerActionOffsetClass = cornerActionInset === 'compact' ? 'bottom-1.5' : 'bottom-2';
|
||||||
|
const orbitTrackWidth = 1;
|
||||||
|
const orbitStrokeWidth = 1.35;
|
||||||
|
const orbitGlowWidth = 3;
|
||||||
|
const orbitInset = orbitTrackWidth / 2;
|
||||||
|
const orbitWidth = Math.max(surfaceShellMetrics.width - orbitTrackWidth, 0);
|
||||||
|
const orbitHeight = Math.max(surfaceShellMetrics.height - orbitTrackWidth, 0);
|
||||||
|
const orbitRadius = Math.max(surfaceShellMetrics.borderRadius - orbitInset, 0);
|
||||||
|
const orbitRight = orbitInset + orbitWidth;
|
||||||
|
const orbitBottom = orbitInset + orbitHeight;
|
||||||
|
const orbitMidX = orbitInset + orbitWidth / 2;
|
||||||
|
const orbitPathData =
|
||||||
|
orbitWidth > 0 && orbitHeight > 0
|
||||||
|
? [
|
||||||
|
`M ${orbitMidX} ${orbitInset}`,
|
||||||
|
`H ${orbitRight - orbitRadius}`,
|
||||||
|
`A ${orbitRadius} ${orbitRadius} 0 0 1 ${orbitRight} ${orbitInset + orbitRadius}`,
|
||||||
|
`V ${orbitBottom - orbitRadius}`,
|
||||||
|
`A ${orbitRadius} ${orbitRadius} 0 0 1 ${orbitRight - orbitRadius} ${orbitBottom}`,
|
||||||
|
`H ${orbitInset + orbitRadius}`,
|
||||||
|
`A ${orbitRadius} ${orbitRadius} 0 0 1 ${orbitInset} ${orbitBottom - orbitRadius}`,
|
||||||
|
`V ${orbitInset + orbitRadius}`,
|
||||||
|
`A ${orbitRadius} ${orbitRadius} 0 0 1 ${orbitInset + orbitRadius} ${orbitInset}`,
|
||||||
|
`H ${orbitMidX}`,
|
||||||
|
'Z',
|
||||||
|
].join(' ')
|
||||||
|
: '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{/* Inner wrapper for textarea + backdrop overlay */}
|
{/* Inner wrapper for textarea + backdrop overlay */}
|
||||||
<div className="relative">
|
<div ref={surfaceShellRef} className={cn('relative rounded-md', surfaceClassName)}>
|
||||||
|
{surfaceDecoration === 'orbit-border' &&
|
||||||
|
surfaceShellMetrics.width > 0 &&
|
||||||
|
surfaceShellMetrics.height > 0 ? (
|
||||||
|
<svg
|
||||||
|
className="message-composer-orbit-svg pointer-events-none absolute inset-0 z-[1] h-full w-full"
|
||||||
|
viewBox={`0 0 ${surfaceShellMetrics.width} ${surfaceShellMetrics.height}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<filter id={orbitGlowId} x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feGaussianBlur stdDeviation="1.35" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<path
|
||||||
|
className="message-composer-orbit-track"
|
||||||
|
d={orbitPathData}
|
||||||
|
pathLength="100"
|
||||||
|
fill="none"
|
||||||
|
strokeWidth={orbitTrackWidth}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="message-composer-orbit-glow"
|
||||||
|
d={orbitPathData}
|
||||||
|
pathLength="100"
|
||||||
|
fill="none"
|
||||||
|
filter={`url(#${orbitGlowId})`}
|
||||||
|
strokeWidth={orbitGlowWidth}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="message-composer-orbit-glow message-composer-orbit-glow-secondary"
|
||||||
|
d={orbitPathData}
|
||||||
|
pathLength="100"
|
||||||
|
fill="none"
|
||||||
|
filter={`url(#${orbitGlowId})`}
|
||||||
|
strokeWidth={orbitGlowWidth}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="message-composer-orbit-path"
|
||||||
|
d={orbitPathData}
|
||||||
|
pathLength="100"
|
||||||
|
fill="none"
|
||||||
|
strokeWidth={orbitStrokeWidth}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="message-composer-orbit-path message-composer-orbit-path-secondary"
|
||||||
|
d={orbitPathData}
|
||||||
|
pathLength="100"
|
||||||
|
fill="none"
|
||||||
|
strokeWidth={orbitStrokeWidth}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
) : null}
|
||||||
{hasOverlay ? (
|
{hasOverlay ? (
|
||||||
<div
|
<div
|
||||||
ref={backdropRef}
|
ref={backdropRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
'pointer-events-none absolute inset-0 z-0 overflow-hidden rounded-md border border-transparent px-3 py-2 text-sm text-[var(--color-text)]',
|
'pointer-events-none absolute inset-0 z-0 overflow-hidden rounded-md border border-transparent px-3 py-2 text-sm text-[var(--color-text)]',
|
||||||
(cornerAction || cornerActionLeft) && 'pb-12'
|
hasCornerActions && cornerInsetClass
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
whiteSpace: 'pre-wrap',
|
whiteSpace: 'pre-wrap',
|
||||||
|
|
@ -1204,7 +1338,7 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
|
||||||
onKeyDown={composedHandleKeyDown}
|
onKeyDown={composedHandleKeyDown}
|
||||||
onSelect={composedHandleSelect}
|
onSelect={composedHandleSelect}
|
||||||
{...textareaProps}
|
{...textareaProps}
|
||||||
className={cn(className, (cornerAction || cornerActionLeft) && 'pb-12')}
|
className={cn(className, hasCornerActions && cornerInsetClass)}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
style={textareaStyle}
|
style={textareaStyle}
|
||||||
/>
|
/>
|
||||||
|
|
@ -1220,25 +1354,34 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{/* Gradient fade overlay before corner action buttons */}
|
{/* Gradient fade overlay before corner action buttons */}
|
||||||
{cornerAction || cornerActionLeft ? (
|
{hasCornerActions ? (
|
||||||
<div
|
<div
|
||||||
className="pointer-events-none absolute inset-x-0 bottom-0 z-[15] rounded-b-md"
|
className="pointer-events-none absolute inset-x-0 bottom-0 z-[15] rounded-b-md"
|
||||||
style={{
|
style={{
|
||||||
height: 48,
|
height: cornerFadeHeight,
|
||||||
background:
|
background: `linear-gradient(to bottom, transparent 0%, ${surfaceFadeColor} 75%)`,
|
||||||
'linear-gradient(to bottom, transparent 0%, var(--color-surface-raised) 75%)',
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{cornerAction ? (
|
{cornerAction ? (
|
||||||
<div className="pointer-events-none absolute bottom-2 right-2 z-20 flex items-end justify-end">
|
<div
|
||||||
|
className={cn(
|
||||||
|
'pointer-events-none absolute right-2 z-20 flex items-end justify-end',
|
||||||
|
cornerActionOffsetClass
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="pointer-events-auto">{cornerAction}</div>
|
<div className="pointer-events-auto">{cornerAction}</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{cornerActionLeft ? (
|
{cornerActionLeft ? (
|
||||||
<div className="pointer-events-none absolute bottom-2 left-2 z-20 flex items-end justify-start">
|
<div
|
||||||
|
className={cn(
|
||||||
|
'pointer-events-none absolute left-2 z-20 flex items-end justify-start',
|
||||||
|
cornerActionOffsetClass
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="pointer-events-auto">{cornerActionLeft}</div>
|
<div className="pointer-events-auto">{cornerActionLeft}</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
||||||
|
|
@ -1323,3 +1323,104 @@ body.theme-transitioning {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes composer-orbit-border {
|
||||||
|
0% {
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
stroke-dashoffset: -100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-composer-orbit-surface {
|
||||||
|
isolation: isolate;
|
||||||
|
--message-composer-orbit-stroke: rgba(129, 140, 248, 0.92);
|
||||||
|
--message-composer-orbit-glow-stroke: rgba(96, 165, 250, 0.62);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-composer-shell {
|
||||||
|
background-color: var(--color-surface-raised);
|
||||||
|
box-shadow:
|
||||||
|
0 8px 24px rgba(0, 0, 0, 0.18),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-composer-orbit-svg {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-composer-orbit-track,
|
||||||
|
.message-composer-orbit-path,
|
||||||
|
.message-composer-orbit-glow {
|
||||||
|
vector-effect: non-scaling-stroke;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-composer-orbit-track {
|
||||||
|
stroke: var(--color-border-emphasis);
|
||||||
|
opacity: 1;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-composer-orbit-path,
|
||||||
|
.message-composer-orbit-glow {
|
||||||
|
stroke-dasharray: 9 91;
|
||||||
|
animation: composer-orbit-border 19s linear infinite;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
will-change: stroke-dashoffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-composer-orbit-path {
|
||||||
|
stroke-width: 1.35px;
|
||||||
|
stroke: var(--message-composer-orbit-stroke);
|
||||||
|
opacity: 0.48;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-composer-orbit-glow {
|
||||||
|
stroke-width: 3px;
|
||||||
|
stroke: var(--message-composer-orbit-glow-stroke);
|
||||||
|
opacity: 0.28;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-composer-orbit-path-secondary,
|
||||||
|
.message-composer-orbit-glow-secondary {
|
||||||
|
animation-delay: -9.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-composer-orbit-surface:focus-within .message-composer-orbit-path,
|
||||||
|
.message-composer-orbit-surface:focus-within .message-composer-orbit-glow {
|
||||||
|
animation-duration: 14.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-composer-orbit-surface:focus-within .message-composer-orbit-path {
|
||||||
|
opacity: 0.62;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-composer-orbit-surface:focus-within .message-composer-orbit-glow {
|
||||||
|
opacity: 0.36;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .message-composer-shell {
|
||||||
|
background-color: #dfdbd4;
|
||||||
|
box-shadow:
|
||||||
|
0 10px 24px rgba(113, 104, 92, 0.08),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .message-composer-orbit-surface {
|
||||||
|
--message-composer-orbit-stroke: rgba(79, 70, 229, 0.76);
|
||||||
|
--message-composer-orbit-glow-stroke: rgba(96, 165, 250, 0.46);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .message-composer-orbit-track {
|
||||||
|
stroke: rgba(138, 130, 118, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.message-composer-orbit-path,
|
||||||
|
.message-composer-orbit-glow {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
||||||
|
|
||||||
import { getWorktreeNavigationState } from '../utils/stateResetHelpers';
|
import { getWorktreeNavigationState } from '../utils/stateResetHelpers';
|
||||||
|
|
||||||
|
import type { TeamMessagesPanelMode } from '@renderer/types/teamMessagesPanelMode';
|
||||||
|
|
||||||
const logger = createLogger('teamSlice');
|
const logger = createLogger('teamSlice');
|
||||||
|
|
||||||
const TEAM_GET_DATA_TIMEOUT_MS = 30_000;
|
const TEAM_GET_DATA_TIMEOUT_MS = 30_000;
|
||||||
|
|
@ -40,9 +42,9 @@ const teamRefreshBurstDiagnostics = new Map<
|
||||||
{ windowStartedAt: number; count: number; lastWarnAt: number }
|
{ windowStartedAt: number; count: number; lastWarnAt: number }
|
||||||
>();
|
>();
|
||||||
const memberSpawnUiEqualLastWarnAtByTeam = new Map<string, number>();
|
const memberSpawnUiEqualLastWarnAtByTeam = new Map<string, number>();
|
||||||
type RefreshTeamDataOptions = {
|
interface RefreshTeamDataOptions {
|
||||||
withDedup?: boolean;
|
withDedup?: boolean;
|
||||||
};
|
}
|
||||||
|
|
||||||
export function isTeamDataRefreshPending(teamName: string): boolean {
|
export function isTeamDataRefreshPending(teamName: string): boolean {
|
||||||
return (
|
return (
|
||||||
|
|
@ -487,9 +489,9 @@ import type {
|
||||||
KanbanColumnId,
|
KanbanColumnId,
|
||||||
LeadActivityState,
|
LeadActivityState,
|
||||||
LeadContextUsage,
|
LeadContextUsage,
|
||||||
PersistedTeamLaunchSummary,
|
|
||||||
MemberSpawnStatusesSnapshot,
|
|
||||||
MemberSpawnStatusEntry,
|
MemberSpawnStatusEntry,
|
||||||
|
MemberSpawnStatusesSnapshot,
|
||||||
|
PersistedTeamLaunchSummary,
|
||||||
SendMessageRequest,
|
SendMessageRequest,
|
||||||
SendMessageResult,
|
SendMessageResult,
|
||||||
TaskChangePresenceState,
|
TaskChangePresenceState,
|
||||||
|
|
@ -852,11 +854,7 @@ function preserveKnownTaskChangePresence(
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousTask = prevTaskById.get(task.id);
|
const previousTask = prevTaskById.get(task.id);
|
||||||
if (
|
if (!previousTask?.changePresence || previousTask.changePresence === 'unknown') {
|
||||||
!previousTask ||
|
|
||||||
!previousTask.changePresence ||
|
|
||||||
previousTask.changePresence === 'unknown'
|
|
||||||
) {
|
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1111,10 +1109,10 @@ export interface TeamSlice {
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
|
||||||
// Messages panel UI state
|
// Messages panel UI state
|
||||||
messagesPanelMode: 'sidebar' | 'inline';
|
messagesPanelMode: TeamMessagesPanelMode;
|
||||||
messagesPanelWidth: number;
|
messagesPanelWidth: number;
|
||||||
sidebarLogsHeight: number;
|
sidebarLogsHeight: number;
|
||||||
setMessagesPanelMode: (mode: 'sidebar' | 'inline') => void;
|
setMessagesPanelMode: (mode: TeamMessagesPanelMode) => void;
|
||||||
setMessagesPanelWidth: (width: number) => void;
|
setMessagesPanelWidth: (width: number) => void;
|
||||||
setSidebarLogsHeight: (height: number) => void;
|
setSidebarLogsHeight: (height: number) => void;
|
||||||
}
|
}
|
||||||
|
|
@ -1395,7 +1393,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
||||||
messagesPanelMode: 'sidebar' as const,
|
messagesPanelMode: 'sidebar' as const,
|
||||||
messagesPanelWidth: 340,
|
messagesPanelWidth: 340,
|
||||||
sidebarLogsHeight: 213,
|
sidebarLogsHeight: 213,
|
||||||
setMessagesPanelMode: (mode: 'sidebar' | 'inline') => set({ messagesPanelMode: mode }),
|
setMessagesPanelMode: (mode: TeamMessagesPanelMode) => set({ messagesPanelMode: mode }),
|
||||||
setMessagesPanelWidth: (width: number) => set({ messagesPanelWidth: width }),
|
setMessagesPanelWidth: (width: number) => set({ messagesPanelWidth: width }),
|
||||||
setSidebarLogsHeight: (height: number) => set({ sidebarLogsHeight: height }),
|
setSidebarLogsHeight: (height: number) => set({ sidebarLogsHeight: height }),
|
||||||
|
|
||||||
|
|
|
||||||
1
src/renderer/types/teamMessagesPanelMode.ts
Normal file
1
src/renderer/types/teamMessagesPanelMode.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export type TeamMessagesPanelMode = 'sidebar' | 'inline' | 'bottom-sheet';
|
||||||
|
|
@ -60,7 +60,8 @@ vi.mock('@renderer/components/ui/button', () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('@renderer/components/ui/tooltip', () => ({
|
vi.mock('@renderer/components/ui/tooltip', () => ({
|
||||||
Tooltip: ({ children }: { children: React.ReactNode }) => React.createElement(React.Fragment, null, children),
|
Tooltip: ({ children }: { children: React.ReactNode }) =>
|
||||||
|
React.createElement(React.Fragment, null, children),
|
||||||
TooltipTrigger: ({ children }: { children: React.ReactNode }) =>
|
TooltipTrigger: ({ children }: { children: React.ReactNode }) =>
|
||||||
React.createElement(React.Fragment, null, children),
|
React.createElement(React.Fragment, null, children),
|
||||||
TooltipContent: ({ children }: { children: React.ReactNode }) =>
|
TooltipContent: ({ children }: { children: React.ReactNode }) =>
|
||||||
|
|
@ -101,6 +102,21 @@ vi.mock('@renderer/components/team/activity/MessageExpandDialog', () => ({
|
||||||
MessageExpandDialog: () => null,
|
MessageExpandDialog: () => null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('react-modal-sheet', () => ({
|
||||||
|
Sheet: Object.assign(
|
||||||
|
({ children }: { children: React.ReactNode }) => React.createElement('div', null, children),
|
||||||
|
{
|
||||||
|
Container: ({ children }: { children: React.ReactNode }) =>
|
||||||
|
React.createElement('div', null, children),
|
||||||
|
Header: ({ children }: { children?: React.ReactNode }) =>
|
||||||
|
React.createElement('div', null, children),
|
||||||
|
DragIndicator: () => React.createElement('div', null, 'drag-indicator'),
|
||||||
|
Content: ({ children }: { children: React.ReactNode }) =>
|
||||||
|
React.createElement('div', null, children),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
import { MessagesPanel } from '@renderer/components/team/messages/MessagesPanel';
|
import { MessagesPanel } from '@renderer/components/team/messages/MessagesPanel';
|
||||||
|
|
||||||
function makeMessage(overrides: Partial<InboxMessage> = {}): InboxMessage {
|
function makeMessage(overrides: Partial<InboxMessage> = {}): InboxMessage {
|
||||||
|
|
@ -163,7 +179,7 @@ describe('MessagesPanel idle summary invariants', () => {
|
||||||
React.createElement(MessagesPanel, {
|
React.createElement(MessagesPanel, {
|
||||||
teamName: 'atlas-hq',
|
teamName: 'atlas-hq',
|
||||||
position: 'sidebar',
|
position: 'sidebar',
|
||||||
onTogglePosition: vi.fn(),
|
onPositionChange: vi.fn(),
|
||||||
members: [],
|
members: [],
|
||||||
tasks: [],
|
tasks: [],
|
||||||
messages,
|
messages,
|
||||||
|
|
@ -214,7 +230,7 @@ describe('MessagesPanel idle summary invariants', () => {
|
||||||
React.createElement(MessagesPanel, {
|
React.createElement(MessagesPanel, {
|
||||||
teamName: 'atlas-hq',
|
teamName: 'atlas-hq',
|
||||||
position: 'sidebar',
|
position: 'sidebar',
|
||||||
onTogglePosition: vi.fn(),
|
onPositionChange: vi.fn(),
|
||||||
members: [],
|
members: [],
|
||||||
tasks: [],
|
tasks: [],
|
||||||
messages,
|
messages,
|
||||||
|
|
@ -234,4 +250,41 @@ describe('MessagesPanel idle summary invariants', () => {
|
||||||
await Promise.resolve();
|
await Promise.resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders the bottom-sheet composer before the status block so input stays pinned near the header', async () => {
|
||||||
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
|
const host = document.createElement('div');
|
||||||
|
const mountPoint = document.createElement('div');
|
||||||
|
host.appendChild(mountPoint);
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(
|
||||||
|
React.createElement(MessagesPanel, {
|
||||||
|
teamName: 'atlas-hq',
|
||||||
|
position: 'bottom-sheet',
|
||||||
|
mountPoint,
|
||||||
|
onPositionChange: vi.fn(),
|
||||||
|
members: [],
|
||||||
|
tasks: [],
|
||||||
|
messages: [makeMessage()],
|
||||||
|
timeWindow: null,
|
||||||
|
teamSessionIds: new Set<string>(),
|
||||||
|
pendingRepliesByMember: {},
|
||||||
|
onPendingReplyChange: vi.fn(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = host.textContent ?? '';
|
||||||
|
expect(text.indexOf('composer')).toBeGreaterThan(-1);
|
||||||
|
expect(text.indexOf('status-block')).toBeGreaterThan(text.indexOf('composer'));
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue