@vertesia/ui 1.5.0 → 1.5.1
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/lib/core/components/SelectList.d.ts.map +1 -1
- package/lib/core/components/SelectList.js +3 -3
- package/lib/core/components/SelectList.js.map +1 -1
- package/lib/core/components/SidePanel.d.ts.map +1 -1
- package/lib/core/components/SidePanel.js +2 -1
- package/lib/core/components/SidePanel.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentConversation.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentConversation.js +51 -17
- package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageInput.d.ts +5 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageInput.d.ts.map +1 -1
- package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js +9 -2
- package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js.map +1 -1
- package/lib/features/agent/chat/hooks/useAgentPlans.d.ts +1 -1
- package/lib/features/agent/chat/hooks/useAgentPlans.d.ts.map +1 -1
- package/lib/features/agent/chat/hooks/useAgentPlans.js +5 -5
- package/lib/features/agent/chat/hooks/useAgentPlans.js.map +1 -1
- package/lib/features/magic-pdf/GroundedExtractionView.js +3 -3
- package/lib/features/magic-pdf/GroundedExtractionView.js.map +1 -1
- package/lib/features/oauth/McpConnectionsButton.d.ts +2 -2
- package/lib/features/oauth/McpConnectionsButton.d.ts.map +1 -1
- package/lib/features/oauth/McpConnectionsButton.js +9 -7
- package/lib/features/oauth/McpConnectionsButton.js.map +1 -1
- package/lib/features/oauth/McpConnectionsDialog.d.ts.map +1 -1
- package/lib/features/oauth/McpConnectionsDialog.js +7 -3
- package/lib/features/oauth/McpConnectionsDialog.js.map +1 -1
- package/lib/features/oauth/useMcpConnections.d.ts +4 -2
- package/lib/features/oauth/useMcpConnections.d.ts.map +1 -1
- package/lib/features/oauth/useMcpConnections.js +15 -9
- package/lib/features/oauth/useMcpConnections.js.map +1 -1
- package/lib/features/store/objects/selection/ObjectsActionContext.d.ts.map +1 -1
- package/lib/features/store/objects/selection/ObjectsActionContext.js +2 -0
- package/lib/features/store/objects/selection/ObjectsActionContext.js.map +1 -1
- package/lib/features/store/objects/selection/SelectionActions.d.ts +4 -2
- package/lib/features/store/objects/selection/SelectionActions.d.ts.map +1 -1
- package/lib/features/store/objects/selection/SelectionActions.js +6 -3
- package/lib/features/store/objects/selection/SelectionActions.js.map +1 -1
- package/lib/features/store/objects/selection/actions/RunIntakeAction.d.ts +8 -0
- package/lib/features/store/objects/selection/actions/RunIntakeAction.d.ts.map +1 -0
- package/lib/features/store/objects/selection/actions/RunIntakeAction.js +65 -0
- package/lib/features/store/objects/selection/actions/RunIntakeAction.js.map +1 -0
- package/lib/features/store/objects/selection/actions/index.d.ts +1 -0
- package/lib/features/store/objects/selection/actions/index.d.ts.map +1 -1
- package/lib/features/store/objects/selection/actions/index.js +1 -0
- package/lib/features/store/objects/selection/actions/index.js.map +1 -1
- package/lib/features/store/types/ContentObjectTypesTable.d.ts.map +1 -1
- package/lib/features/store/types/ContentObjectTypesTable.js +11 -3
- package/lib/features/store/types/ContentObjectTypesTable.js.map +1 -1
- package/lib/features/store/types/IntakePolicyEditor.d.ts.map +1 -1
- package/lib/features/store/types/IntakePolicyEditor.js +14 -14
- package/lib/features/store/types/IntakePolicyEditor.js.map +1 -1
- package/lib/features/store/types/TypeRegistry.d.ts +275 -150
- package/lib/features/store/types/TypeRegistry.d.ts.map +1 -1
- package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts +275 -150
- package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts.map +1 -1
- package/lib/i18n/locales/ar.json +28 -7
- package/lib/i18n/locales/de.json +24 -7
- package/lib/i18n/locales/en.json +24 -7
- package/lib/i18n/locales/es.json +24 -7
- package/lib/i18n/locales/fr.json +25 -7
- package/lib/i18n/locales/it.json +24 -7
- package/lib/i18n/locales/ja.json +24 -7
- package/lib/i18n/locales/ko.json +24 -7
- package/lib/i18n/locales/pt.json +24 -7
- package/lib/i18n/locales/ru.json +26 -7
- package/lib/i18n/locales/tr.json +24 -7
- package/lib/i18n/locales/zh-TW.json +24 -7
- package/lib/i18n/locales/zh.json +24 -7
- package/lib/shell/apps/AppInstallationProvider.d.ts +4 -4
- package/lib/shell/login/SignInIcons.d.ts.map +1 -1
- package/lib/shell/login/SignInIcons.js +2 -1
- package/lib/shell/login/SignInIcons.js.map +1 -1
- package/lib/shell/login/SignInPasswordStep.d.ts +9 -0
- package/lib/shell/login/SignInPasswordStep.d.ts.map +1 -0
- package/lib/shell/login/SignInPasswordStep.js +58 -0
- package/lib/shell/login/SignInPasswordStep.js.map +1 -0
- package/lib/shell/login/SignInPrimitives.d.ts +13 -0
- package/lib/shell/login/SignInPrimitives.d.ts.map +1 -1
- package/lib/shell/login/SignInPrimitives.js +11 -1
- package/lib/shell/login/SignInPrimitives.js.map +1 -1
- package/lib/shell/login/SignInProvidersStep.d.ts +3 -1
- package/lib/shell/login/SignInProvidersStep.d.ts.map +1 -1
- package/lib/shell/login/SignInProvidersStep.js +4 -2
- package/lib/shell/login/SignInProvidersStep.js.map +1 -1
- package/lib/shell/login/SignInReturningStep.d.ts +3 -1
- package/lib/shell/login/SignInReturningStep.d.ts.map +1 -1
- package/lib/shell/login/SignInReturningStep.js +18 -8
- package/lib/shell/login/SignInReturningStep.js.map +1 -1
- package/lib/shell/login/SignInTenantStep.d.ts +3 -1
- package/lib/shell/login/SignInTenantStep.d.ts.map +1 -1
- package/lib/shell/login/SignInTenantStep.js +2 -2
- package/lib/shell/login/SignInTenantStep.js.map +1 -1
- package/lib/shell/login/SignInWithProviderButton.d.ts +5 -3
- package/lib/shell/login/SignInWithProviderButton.d.ts.map +1 -1
- package/lib/shell/login/SignInWithProviderButton.js +8 -7
- package/lib/shell/login/SignInWithProviderButton.js.map +1 -1
- package/lib/shell/login/SigninScreen.d.ts.map +1 -1
- package/lib/shell/login/SigninScreen.js +13 -4
- package/lib/shell/login/SigninScreen.js.map +1 -1
- package/lib/shell/login/signInUtils.d.ts +20 -4
- package/lib/shell/login/signInUtils.d.ts.map +1 -1
- package/lib/shell/login/signInUtils.js +47 -1
- package/lib/shell/login/signInUtils.js.map +1 -1
- package/lib/vertesia-ui-core.js +1 -1
- package/lib/vertesia-ui-core.js.map +1 -1
- package/lib/vertesia-ui-features.js +14 -14
- package/lib/vertesia-ui-features.js.map +1 -1
- package/lib/vertesia-ui-i18n.BoNE1APG.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.BoNE1APG.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.BoPtiSPb.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.BoPtiSPb.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.C4BBW5v5.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.C4BBW5v5.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.C81wuNkY.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.C81wuNkY.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.C9GipKIH.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.C9GipKIH.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.CWsoS0W9.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.CWsoS0W9.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.Cr9NtzZB.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.Cr9NtzZB.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.DEpkHOD5.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.DEpkHOD5.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.DPXaRKFO.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.DPXaRKFO.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.D_QNm1Xg.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.D_QNm1Xg.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.F26Cc0f7.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.F26Cc0f7.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.YkXIESV_.chunk.js +5 -0
- package/lib/vertesia-ui-i18n.YkXIESV_.chunk.js.map +1 -0
- package/lib/vertesia-ui-i18n.js +3 -3
- package/lib/vertesia-ui-i18n.js.map +1 -1
- package/lib/vertesia-ui-shell.js +15 -9
- package/lib/vertesia-ui-shell.js.map +1 -1
- package/lib/widgets/xml-viewer/hooks/useCollapsible.d.ts +1 -1
- package/package.json +7 -7
- package/src/core/components/SelectList.tsx +5 -2
- package/src/core/components/SidePanel.tsx +65 -60
- package/src/features/agent/chat/ModernAgentConversation.test.tsx +150 -1
- package/src/features/agent/chat/ModernAgentConversation.tsx +59 -21
- package/src/features/agent/chat/ModernAgentOutput/MessageInput.tsx +17 -1
- package/src/features/agent/chat/hooks/useAgentPlans.test.ts +52 -0
- package/src/features/agent/chat/hooks/useAgentPlans.ts +5 -5
- package/src/features/magic-pdf/GroundedExtractionView.tsx +3 -3
- package/src/features/oauth/McpConnectionsButton.test.tsx +56 -0
- package/src/features/oauth/McpConnectionsButton.tsx +9 -6
- package/src/features/oauth/McpConnectionsDialog.tsx +14 -5
- package/src/features/oauth/useMcpConnections.test.ts +10 -0
- package/src/features/oauth/useMcpConnections.ts +18 -10
- package/src/features/store/objects/selection/ObjectsActionContext.tsx +2 -0
- package/src/features/store/objects/selection/SelectionActions.tsx +13 -6
- package/src/features/store/objects/selection/actions/RunIntakeAction.tsx +69 -0
- package/src/features/store/objects/selection/actions/index.ts +1 -0
- package/src/features/store/types/ContentObjectTypesTable.tsx +20 -3
- package/src/features/store/types/IntakePolicyEditor.tsx +33 -28
- package/src/i18n/locales/ar.json +28 -7
- package/src/i18n/locales/de.json +24 -7
- package/src/i18n/locales/en.json +24 -7
- package/src/i18n/locales/es.json +24 -7
- package/src/i18n/locales/fr.json +25 -7
- package/src/i18n/locales/it.json +24 -7
- package/src/i18n/locales/ja.json +24 -7
- package/src/i18n/locales/ko.json +24 -7
- package/src/i18n/locales/pt.json +24 -7
- package/src/i18n/locales/ru.json +26 -7
- package/src/i18n/locales/tr.json +24 -7
- package/src/i18n/locales/zh-TW.json +24 -7
- package/src/i18n/locales/zh.json +24 -7
- package/src/shell/login/SignInIcons.tsx +2 -1
- package/src/shell/login/SignInPasswordRequired.test.tsx +82 -0
- package/src/shell/login/SignInPasswordStep.test.tsx +71 -0
- package/src/shell/login/SignInPasswordStep.tsx +103 -0
- package/src/shell/login/SignInPrimitives.tsx +53 -1
- package/src/shell/login/SignInProvidersStep.tsx +8 -4
- package/src/shell/login/SignInReturningStep.tsx +20 -8
- package/src/shell/login/SignInTenantStep.tsx +6 -2
- package/src/shell/login/SignInWithProviderButton.tsx +11 -7
- package/src/shell/login/SigninScreen.tsx +32 -3
- package/src/shell/login/signInPassword.test.ts +133 -0
- package/src/shell/login/signInUtils.ts +60 -7
- package/lib/features/store/objects/selection/actions/StartWorkflowButton.d.ts +0 -7
- package/lib/features/store/objects/selection/actions/StartWorkflowButton.d.ts.map +0 -1
- package/lib/features/store/objects/selection/actions/StartWorkflowButton.js +0 -51
- package/lib/features/store/objects/selection/actions/StartWorkflowButton.js.map +0 -1
- package/lib/vertesia-ui-i18n.BfN7iOHb.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BfN7iOHb.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BjH2zEZw.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BjH2zEZw.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.BjSwAZXl.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.BjSwAZXl.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.C7PUm5Au.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.C7PUm5Au.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.CF_1mUS5.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.CF_1mUS5.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.ChfUl3_-.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.ChfUl3_-.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.D-Sk-5d1.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.D-Sk-5d1.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.D4rnu1_S.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.D4rnu1_S.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DDf0-laK.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DDf0-laK.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DN79y06J.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DN79y06J.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DWLIciYQ.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DWLIciYQ.chunk.js.map +0 -1
- package/lib/vertesia-ui-i18n.DvF_YaAJ.chunk.js +0 -5
- package/lib/vertesia-ui-i18n.DvF_YaAJ.chunk.js.map +0 -1
- package/src/features/store/objects/selection/actions/StartWorkflowButton.tsx +0 -104
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertesia/ui",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Vertesia UI components and and hooks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -92,11 +92,11 @@
|
|
|
92
92
|
"vega": "^6.3.1",
|
|
93
93
|
"vega-embed": "^7.1.0",
|
|
94
94
|
"vega-lite": "^6.4.3",
|
|
95
|
-
"@vertesia/
|
|
96
|
-
"@vertesia/
|
|
97
|
-
"@vertesia/
|
|
98
|
-
"@vertesia/
|
|
99
|
-
"@vertesia/
|
|
95
|
+
"@vertesia/client": "1.5.1",
|
|
96
|
+
"@vertesia/fusion-ux": "1.5.1",
|
|
97
|
+
"@vertesia/rich-text": "1.5.1",
|
|
98
|
+
"@vertesia/json": "1.5.1",
|
|
99
|
+
"@vertesia/common": "1.5.1"
|
|
100
100
|
},
|
|
101
101
|
"devDependencies": {
|
|
102
102
|
"@testing-library/react": "^16.3.2",
|
|
@@ -179,7 +179,7 @@
|
|
|
179
179
|
"url": "https://github.com/vertesia/composableai.git",
|
|
180
180
|
"directory": "packages/ui"
|
|
181
181
|
},
|
|
182
|
-
"gitHead": "
|
|
182
|
+
"gitHead": "7ab3acd95af12b8c254e1b3852e67d5078790f98",
|
|
183
183
|
"scripts": {
|
|
184
184
|
"clean:lib": "rimraf ./lib ./tsconfig.tsbuildinfo",
|
|
185
185
|
"clean": "rimraf ./node_modules ./lib ./tsconfig.tsbuildinfo",
|
|
@@ -88,6 +88,7 @@ export function SelectList<T>({
|
|
|
88
88
|
onSelect={onSelect}
|
|
89
89
|
layout={layout}
|
|
90
90
|
noCheck={noCheck}
|
|
91
|
+
isSelected={isSelected}
|
|
91
92
|
/>
|
|
92
93
|
);
|
|
93
94
|
})}
|
|
@@ -99,17 +100,19 @@ interface SelectListOptionProps<T> {
|
|
|
99
100
|
onSelect: (value: T) => void;
|
|
100
101
|
layout: OptionLayout;
|
|
101
102
|
noCheck?: boolean;
|
|
103
|
+
isSelected: boolean;
|
|
102
104
|
}
|
|
103
105
|
|
|
104
|
-
function SelectListOption<T>({ option, onSelect, layout, noCheck }: SelectListOptionProps<T>) {
|
|
106
|
+
function SelectListOption<T>({ option, onSelect, layout, noCheck, isSelected }: SelectListOptionProps<T>) {
|
|
105
107
|
return (
|
|
106
108
|
<Button
|
|
107
109
|
variant="unstyled"
|
|
108
110
|
size="none"
|
|
109
111
|
className={clsx(
|
|
110
|
-
'group !
|
|
112
|
+
'group flex! w-full items-center cursor-pointer gap-x-2 hover:bg-muted',
|
|
111
113
|
layout.reverse && 'flex-row-reverse',
|
|
112
114
|
layout.className,
|
|
115
|
+
isSelected && 'bg-muted',
|
|
113
116
|
)}
|
|
114
117
|
onClick={() => onSelect(option)}
|
|
115
118
|
>
|
|
@@ -2,6 +2,7 @@ import { AnimatePresence, motion } from 'framer-motion';
|
|
|
2
2
|
import { Minus, X } from 'lucide-react';
|
|
3
3
|
import type React from 'react';
|
|
4
4
|
import { useState } from 'react';
|
|
5
|
+
import { Portal } from './Portal.js';
|
|
5
6
|
import { Button } from './shadcn/button';
|
|
6
7
|
|
|
7
8
|
interface SidePanelProps {
|
|
@@ -72,78 +73,82 @@ export function SidePanel({
|
|
|
72
73
|
const initialX = isLeft ? '-100%' : '100%';
|
|
73
74
|
|
|
74
75
|
return (
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
76
|
+
<Portal>
|
|
77
|
+
<AnimatePresence>
|
|
78
|
+
{isOpen && (
|
|
79
|
+
<div className="relative z-20">
|
|
80
|
+
{/* Backdrop */}
|
|
81
|
+
{backdrop && (
|
|
82
|
+
<motion.div
|
|
83
|
+
className="fixed inset-0 bg-black/70"
|
|
84
|
+
initial={{ opacity: 0 }}
|
|
85
|
+
animate={{ opacity: 1 }}
|
|
86
|
+
exit={{ opacity: 0 }}
|
|
87
|
+
onClick={onClose}
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
88
90
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
>
|
|
94
|
-
<motion.div
|
|
95
|
-
className={`pointer-events-auto ${borderClass}`}
|
|
96
|
-
style={{ width: `${_panelWidth}px` }}
|
|
97
|
-
initial={{ x: initialX }}
|
|
98
|
-
animate={{ x: 0 }}
|
|
99
|
-
exit={{ x: initialX }}
|
|
100
|
-
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
|
91
|
+
<div className={`fixed inset-y-0 ${positionClass} overflow-hidden`}>
|
|
92
|
+
<div className="absolute inset-0 overflow-hidden">
|
|
93
|
+
<div
|
|
94
|
+
className={`pointer-events-none fixed inset-y-0 ${positionClass} flex max-w-full ${paddingClass}`}
|
|
101
95
|
>
|
|
102
|
-
<div
|
|
103
|
-
{
|
|
104
|
-
{
|
|
105
|
-
|
|
106
|
-
|
|
96
|
+
<motion.div
|
|
97
|
+
className={`pointer-events-auto ${borderClass}`}
|
|
98
|
+
style={{ width: `${_panelWidth}px` }}
|
|
99
|
+
initial={{ x: initialX }}
|
|
100
|
+
animate={{ x: 0 }}
|
|
101
|
+
exit={{ x: initialX }}
|
|
102
|
+
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
|
103
|
+
>
|
|
104
|
+
<div className="relative flex h-full">
|
|
105
|
+
{/* Drag Handle */}
|
|
106
|
+
{resizable && (
|
|
107
|
+
// biome-ignore lint/a11y/noStaticElementInteractions: drag handle is pointer-only (no keyboard equivalent for continuous resize); ARIA role omitted to avoid useAriaPropsForRole false positive.
|
|
108
|
+
// biome-ignore lint/a11y/useAriaPropsSupportedByRole: aria-label kept for AT users; div has no semantic role because separator/slider would require valuenow/min/max
|
|
109
|
+
<div
|
|
110
|
+
aria-label="Resize panel"
|
|
111
|
+
className={`absolute ${dragHandleClass} top-0 bottom-0 w-3 z-25 cursor-ew-resize hover:bg-primary transition-colors flex items-center justify-center`}
|
|
112
|
+
onMouseDown={handleDragStart}
|
|
113
|
+
>
|
|
114
|
+
<Minus className="rotate-90 font-semibold" strokeWidth={4} />
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
107
117
|
<div
|
|
108
|
-
|
|
109
|
-
className={`absolute ${dragHandleClass} top-0 bottom-0 w-3 z-25 cursor-ew-resize hover:bg-primary transition-colors flex items-center justify-center`}
|
|
110
|
-
onMouseDown={handleDragStart}
|
|
118
|
+
className={`flex-1 min-w-0 flex flex-col bg-background shadow-xl ${className}`}
|
|
111
119
|
>
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
<h2 className="w-full text-base font-semibold leading-6">
|
|
123
|
-
<div className="text-2xl">{title ?? ''}</div>
|
|
124
|
-
</h2>
|
|
125
|
-
<div className="ms-3 flex h-7 items-center">
|
|
126
|
-
<CloseButton onClose={onClose} />
|
|
120
|
+
{/* Sticky header */}
|
|
121
|
+
{title && (
|
|
122
|
+
<div className="sticky top-0 z-10 bg-background px-2 sm:px-4 py-2 border-b">
|
|
123
|
+
<div className="flex items-start justify-between">
|
|
124
|
+
<h2 className="w-full text-base font-semibold leading-6">
|
|
125
|
+
<div className="text-2xl">{title ?? ''}</div>
|
|
126
|
+
</h2>
|
|
127
|
+
<div className="ms-3 flex h-7 items-center">
|
|
128
|
+
<CloseButton onClose={onClose} />
|
|
129
|
+
</div>
|
|
127
130
|
</div>
|
|
128
131
|
</div>
|
|
129
|
-
|
|
130
|
-
)}
|
|
132
|
+
)}
|
|
131
133
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
{/* Scrollable content */}
|
|
135
|
+
<div
|
|
136
|
+
className={
|
|
137
|
+
contentClassName ?? 'flex-1 overflow-auto px-2 sm:px-4 py-4'
|
|
138
|
+
}
|
|
139
|
+
>
|
|
140
|
+
<div className="w-full h-full flex-1 flex flex-col">{children}</div>
|
|
141
|
+
</div>
|
|
137
142
|
</div>
|
|
138
143
|
</div>
|
|
139
|
-
</div>
|
|
140
|
-
</
|
|
144
|
+
</motion.div>
|
|
145
|
+
</div>
|
|
141
146
|
</div>
|
|
142
147
|
</div>
|
|
143
148
|
</div>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
</
|
|
149
|
+
)}
|
|
150
|
+
</AnimatePresence>
|
|
151
|
+
</Portal>
|
|
147
152
|
);
|
|
148
153
|
}
|
|
149
154
|
|
|
@@ -61,6 +61,8 @@ vi.mock('./ModernAgentOutput/Header', () => ({
|
|
|
61
61
|
vi.mock('./ModernAgentOutput/MessageInput', () => ({
|
|
62
62
|
default: (props: {
|
|
63
63
|
onSend: (message: string) => void;
|
|
64
|
+
value?: string;
|
|
65
|
+
onValueChange?: (value: string) => void;
|
|
64
66
|
activeTaskCount?: number;
|
|
65
67
|
activeWorkstreams?: Array<{ workstream_id: string; status: string }>;
|
|
66
68
|
onSelectWorkstream?: (workstreamId: string) => void;
|
|
@@ -81,6 +83,11 @@ vi.mock('./ModernAgentOutput/MessageInput', () => ({
|
|
|
81
83
|
return (
|
|
82
84
|
<div>
|
|
83
85
|
{props.approvalModeSlot}
|
|
86
|
+
<textarea
|
|
87
|
+
aria-label="composer draft"
|
|
88
|
+
value={props.value}
|
|
89
|
+
onChange={(event) => props.onValueChange?.(event.target.value)}
|
|
90
|
+
/>
|
|
84
91
|
<button type="button" onClick={() => props.onSend('follow up')}>
|
|
85
92
|
composer send
|
|
86
93
|
</button>
|
|
@@ -1278,7 +1285,7 @@ describe('ModernAgentConversation send handling', () => {
|
|
|
1278
1285
|
expect.objectContaining({ workstream_id: 'beta', status: 'running' }),
|
|
1279
1286
|
expect.objectContaining({ workstream_id: 'gamma', status: 'completed' }),
|
|
1280
1287
|
]);
|
|
1281
|
-
expect(latestRightPanelProps.activeTab).toBe('
|
|
1288
|
+
expect(latestRightPanelProps.activeTab).toBe('workstreams');
|
|
1282
1289
|
expect(latestMessageInputProps.activeTaskCount).toBe(2);
|
|
1283
1290
|
expect(latestMessageInputProps.activeWorkstreams).toEqual([
|
|
1284
1291
|
expect.objectContaining({ workstream_id: 'alpha', status: 'running' }),
|
|
@@ -1706,6 +1713,80 @@ describe('ModernAgentConversation send handling', () => {
|
|
|
1706
1713
|
);
|
|
1707
1714
|
});
|
|
1708
1715
|
|
|
1716
|
+
it('restores the composer draft after an option response without sending it as part of that response', async () => {
|
|
1717
|
+
const requestMessage = {
|
|
1718
|
+
...createMessage(AgentMessageType.REQUEST_INPUT, 'What is your favorite color?'),
|
|
1719
|
+
details: {
|
|
1720
|
+
request_id: 'ask-user-1',
|
|
1721
|
+
ux: {
|
|
1722
|
+
options: [
|
|
1723
|
+
{ id: 'red', label: 'Red' },
|
|
1724
|
+
{ id: 'blue', label: 'Blue' },
|
|
1725
|
+
],
|
|
1726
|
+
},
|
|
1727
|
+
},
|
|
1728
|
+
};
|
|
1729
|
+
mockStreamState({ messages: [], isCompleted: false, agentRunStatus: 'RUNNING' });
|
|
1730
|
+
|
|
1731
|
+
const view = renderConversation({ hideMessageInput: false });
|
|
1732
|
+
fireEvent.change(screen.getByRole('textbox', { name: 'composer draft' }), {
|
|
1733
|
+
target: { value: 'My pending message' },
|
|
1734
|
+
});
|
|
1735
|
+
|
|
1736
|
+
mockStreamState({ messages: [requestMessage], isCompleted: false, agentRunStatus: 'RUNNING' });
|
|
1737
|
+
view.rerender(
|
|
1738
|
+
<ModernAgentConversation
|
|
1739
|
+
agentRunId="agent-run-1"
|
|
1740
|
+
title="Agent"
|
|
1741
|
+
hideHeader
|
|
1742
|
+
hideMessageInput={false}
|
|
1743
|
+
showRightPanel={false}
|
|
1744
|
+
/>,
|
|
1745
|
+
);
|
|
1746
|
+
|
|
1747
|
+
await act(async () => {
|
|
1748
|
+
fireEvent.click(screen.getByRole('button', { name: 'Blue' }));
|
|
1749
|
+
await Promise.resolve();
|
|
1750
|
+
});
|
|
1751
|
+
await waitFor(() => {
|
|
1752
|
+
expect(mocks.sendSignal).toHaveBeenCalledWith(
|
|
1753
|
+
'agent-run-1',
|
|
1754
|
+
'UserInput',
|
|
1755
|
+
expect.objectContaining({ message: 'blue' }),
|
|
1756
|
+
);
|
|
1757
|
+
});
|
|
1758
|
+
|
|
1759
|
+
mockStreamState({
|
|
1760
|
+
messages: [
|
|
1761
|
+
requestMessage,
|
|
1762
|
+
{
|
|
1763
|
+
...createMessage(AgentMessageType.QUESTION, 'blue'),
|
|
1764
|
+
details: { request_input_response: { request_id: 'ask-user-1' } },
|
|
1765
|
+
},
|
|
1766
|
+
],
|
|
1767
|
+
isCompleted: false,
|
|
1768
|
+
agentRunStatus: 'RUNNING',
|
|
1769
|
+
});
|
|
1770
|
+
view.rerender(
|
|
1771
|
+
<ModernAgentConversation
|
|
1772
|
+
agentRunId="agent-run-1"
|
|
1773
|
+
title="Agent"
|
|
1774
|
+
hideHeader
|
|
1775
|
+
hideMessageInput={false}
|
|
1776
|
+
showRightPanel={false}
|
|
1777
|
+
/>,
|
|
1778
|
+
);
|
|
1779
|
+
|
|
1780
|
+
expect((screen.getByRole('textbox', { name: 'composer draft' }) as HTMLTextAreaElement).value).toBe(
|
|
1781
|
+
'My pending message',
|
|
1782
|
+
);
|
|
1783
|
+
expect(mocks.sendSignal).not.toHaveBeenCalledWith(
|
|
1784
|
+
'agent-run-1',
|
|
1785
|
+
'UserInput',
|
|
1786
|
+
expect.objectContaining({ message: 'My pending message' }),
|
|
1787
|
+
);
|
|
1788
|
+
});
|
|
1789
|
+
|
|
1709
1790
|
it('does not resurrect an answered request overlay after remounting before the persisted echo arrives', async () => {
|
|
1710
1791
|
const requestMessage = {
|
|
1711
1792
|
...createMessage(AgentMessageType.REQUEST_INPUT, 'What is your favorite color?'),
|
|
@@ -1960,6 +2041,74 @@ describe('ModernAgentConversation send handling', () => {
|
|
|
1960
2041
|
);
|
|
1961
2042
|
});
|
|
1962
2043
|
|
|
2044
|
+
it('continues a run that ended while shown as running when the send is rejected with 409', async () => {
|
|
2045
|
+
mockStreamState({
|
|
2046
|
+
messages: [createMessage(AgentMessageType.ANSWER, 'still running')],
|
|
2047
|
+
agentRunStatus: 'RUNNING',
|
|
2048
|
+
});
|
|
2049
|
+
mocks.sendSignal
|
|
2050
|
+
.mockRejectedValueOnce(Object.assign(new Error('This agent run has ended.'), { status: 409 }))
|
|
2051
|
+
.mockResolvedValueOnce({});
|
|
2052
|
+
|
|
2053
|
+
renderConversation({ onRestart: vi.fn() });
|
|
2054
|
+
mocks.retrieve.mockResolvedValue({ status: 'completed' });
|
|
2055
|
+
|
|
2056
|
+
fireEvent.click(screen.getByRole('button', { name: 'inline send' }));
|
|
2057
|
+
|
|
2058
|
+
await waitFor(() => {
|
|
2059
|
+
expect(mocks.sendSignal).toHaveBeenCalledTimes(2);
|
|
2060
|
+
});
|
|
2061
|
+
|
|
2062
|
+
expect(mocks.restart).toHaveBeenCalledWith('agent-run-1');
|
|
2063
|
+
expect(mocks.reconnect).toHaveBeenCalledTimes(1);
|
|
2064
|
+
expect(mocks.restart.mock.invocationCallOrder[0]).toBeLessThan(mocks.sendSignal.mock.invocationCallOrder[1]);
|
|
2065
|
+
await waitFor(() => {
|
|
2066
|
+
expect(mocks.updateOptimisticMessageStatus).toHaveBeenCalledWith(expect.any(String), 'received');
|
|
2067
|
+
});
|
|
2068
|
+
expect(mocks.updateOptimisticMessageStatus).not.toHaveBeenCalledWith(expect.any(String), 'failed');
|
|
2069
|
+
});
|
|
2070
|
+
|
|
2071
|
+
it('shows how the run ended instead of continuing it when it failed', async () => {
|
|
2072
|
+
mockStreamState({
|
|
2073
|
+
messages: [createMessage(AgentMessageType.ANSWER, 'still running')],
|
|
2074
|
+
agentRunStatus: 'RUNNING',
|
|
2075
|
+
});
|
|
2076
|
+
mocks.sendSignal.mockRejectedValue(Object.assign(new Error('This agent run has ended.'), { status: 409 }));
|
|
2077
|
+
|
|
2078
|
+
renderConversation({ onRestart: vi.fn() });
|
|
2079
|
+
mocks.retrieve.mockResolvedValue({ status: 'failed' });
|
|
2080
|
+
|
|
2081
|
+
fireEvent.click(screen.getByRole('button', { name: 'inline send' }));
|
|
2082
|
+
|
|
2083
|
+
await waitFor(() => {
|
|
2084
|
+
expect(mocks.updateOptimisticMessageStatus).toHaveBeenCalledWith(expect.any(String), 'failed');
|
|
2085
|
+
});
|
|
2086
|
+
expect(mocks.restart).not.toHaveBeenCalled();
|
|
2087
|
+
expect(mocks.reconnect).toHaveBeenCalledTimes(1);
|
|
2088
|
+
expect(mocks.sendSignal).toHaveBeenCalledTimes(1);
|
|
2089
|
+
});
|
|
2090
|
+
|
|
2091
|
+
it('does not restart a run that answers 409 while still running', async () => {
|
|
2092
|
+
mockStreamState({
|
|
2093
|
+
messages: [createMessage(AgentMessageType.ANSWER, 'still running')],
|
|
2094
|
+
agentRunStatus: 'RUNNING',
|
|
2095
|
+
});
|
|
2096
|
+
mocks.sendSignal.mockRejectedValue(
|
|
2097
|
+
Object.assign(new Error('Agent run workflow is not ready yet; retry shortly.'), { status: 409 }),
|
|
2098
|
+
);
|
|
2099
|
+
|
|
2100
|
+
renderConversation({ onRestart: vi.fn() });
|
|
2101
|
+
mocks.retrieve.mockResolvedValue({ status: 'running' });
|
|
2102
|
+
|
|
2103
|
+
fireEvent.click(screen.getByRole('button', { name: 'inline send' }));
|
|
2104
|
+
|
|
2105
|
+
await waitFor(() => {
|
|
2106
|
+
expect(mocks.updateOptimisticMessageStatus).toHaveBeenCalledWith(expect.any(String), 'failed');
|
|
2107
|
+
});
|
|
2108
|
+
expect(mocks.restart).not.toHaveBeenCalled();
|
|
2109
|
+
expect(mocks.reconnect).not.toHaveBeenCalled();
|
|
2110
|
+
});
|
|
2111
|
+
|
|
1963
2112
|
it('marks a sent message as sending and then received', async () => {
|
|
1964
2113
|
mockStreamState({
|
|
1965
2114
|
messages: [createMessage(AgentMessageType.ANSWER, 'still running')],
|
|
@@ -150,6 +150,24 @@ async function closeStagedFileBatch(
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
const TERMINAL_WORKFLOW_STATUSES = new Set(['COMPLETED', 'FAILED', 'CANCELED', 'CANCELLED', 'TERMINATED', 'TIMED_OUT']);
|
|
154
|
+
|
|
155
|
+
function isTerminalWorkflowStatus(status: string | null | undefined): boolean {
|
|
156
|
+
return !!status && TERMINAL_WORKFLOW_STATUSES.has(status.toUpperCase());
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Whether the next message can continue an ended run by restarting it. FAILED runs are excluded: a
|
|
161
|
+
* failed run is a dead end, so the failed box and its explicit Restart action are shown instead.
|
|
162
|
+
*/
|
|
163
|
+
function canContinueWorkflowStatus(status: string | null | undefined, canRestart: boolean): boolean {
|
|
164
|
+
return canRestart && isTerminalWorkflowStatus(status) && status?.toUpperCase() !== 'FAILED';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isConflictError(err: unknown): boolean {
|
|
168
|
+
return typeof err === 'object' && err !== null && 'status' in err && err.status === 409;
|
|
169
|
+
}
|
|
170
|
+
|
|
153
171
|
function getTimestampMs(timestamp: number | string | undefined): number {
|
|
154
172
|
if (typeof timestamp === 'number') return timestamp;
|
|
155
173
|
if (!timestamp) return Date.now();
|
|
@@ -1553,7 +1571,7 @@ function ModernAgentConversationInner({
|
|
|
1553
1571
|
showInput,
|
|
1554
1572
|
showSlidingPanel,
|
|
1555
1573
|
setShowSlidingPanel,
|
|
1556
|
-
} = useAgentPlans(messages, interactive
|
|
1574
|
+
} = useAgentPlans(messages, interactive);
|
|
1557
1575
|
|
|
1558
1576
|
const {
|
|
1559
1577
|
openDocuments,
|
|
@@ -1585,6 +1603,9 @@ function ModernAgentConversationInner({
|
|
|
1585
1603
|
const conversationRef = useRef<HTMLDivElement | null>(null);
|
|
1586
1604
|
const conversationLayoutRef = useRef<HTMLDivElement | null>(null);
|
|
1587
1605
|
const [isSending, setIsSending] = useState(false);
|
|
1606
|
+
// Request-input overlays replace the composer while the user chooses a response. Keep the
|
|
1607
|
+
// ordinary composer draft here so that temporary unmount does not discard it.
|
|
1608
|
+
const [composerValue, setComposerValue] = useState('');
|
|
1588
1609
|
const [isCompactingContext, setIsCompactingContext] = useState(false);
|
|
1589
1610
|
const [internalViewMode, setInternalViewMode] = useState<AgentConversationViewMode>('sliding');
|
|
1590
1611
|
const viewMode = controlledViewMode ?? internalViewMode;
|
|
@@ -1659,17 +1680,10 @@ function ModernAgentConversationInner({
|
|
|
1659
1680
|
return agentRunStatus;
|
|
1660
1681
|
}, [lastMainMessage, agentRunStatus]);
|
|
1661
1682
|
|
|
1662
|
-
const isWorkflowTerminal = useMemo(
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
normalizedStatus === 'FAILED' ||
|
|
1667
|
-
normalizedStatus === 'CANCELED' ||
|
|
1668
|
-
normalizedStatus === 'CANCELLED' ||
|
|
1669
|
-
normalizedStatus === 'TERMINATED' ||
|
|
1670
|
-
normalizedStatus === 'TIMED_OUT'
|
|
1671
|
-
);
|
|
1672
|
-
}, [effectiveWorkflowStatus]);
|
|
1683
|
+
const isWorkflowTerminal = useMemo(
|
|
1684
|
+
() => isTerminalWorkflowStatus(effectiveWorkflowStatus),
|
|
1685
|
+
[effectiveWorkflowStatus],
|
|
1686
|
+
);
|
|
1673
1687
|
|
|
1674
1688
|
// When a terminal conversation can be restarted (host provided a restart handler),
|
|
1675
1689
|
// we keep the composer visible and seamlessly resume the agent on the next message
|
|
@@ -1677,8 +1691,8 @@ function ModernAgentConversationInner({
|
|
|
1677
1691
|
// FAILED runs are excluded: a failed run is a dead end, so we surface the failed box /
|
|
1678
1692
|
// `failedAction` (e.g. an explicit Restart button) instead of silently resuming.
|
|
1679
1693
|
const canContinueConversation = useMemo(
|
|
1680
|
-
() =>
|
|
1681
|
-
[
|
|
1694
|
+
() => canContinueWorkflowStatus(effectiveWorkflowStatus, !!onRestart),
|
|
1695
|
+
[effectiveWorkflowStatus, onRestart],
|
|
1682
1696
|
);
|
|
1683
1697
|
const shouldRenderMessageInputArea = !hideMessageInput || canContinueConversation;
|
|
1684
1698
|
|
|
@@ -1706,6 +1720,8 @@ function ModernAgentConversationInner({
|
|
|
1706
1720
|
isWorkflowTerminalRef.current = isWorkflowTerminal;
|
|
1707
1721
|
const canContinueConversationRef = useRef(canContinueConversation);
|
|
1708
1722
|
canContinueConversationRef.current = canContinueConversation;
|
|
1723
|
+
const canRestartRef = useRef(!!onRestart);
|
|
1724
|
+
canRestartRef.current = !!onRestart;
|
|
1709
1725
|
|
|
1710
1726
|
// ────────────────────────────────────────────
|
|
1711
1727
|
// Computed values
|
|
@@ -1937,9 +1953,10 @@ function ModernAgentConversationInner({
|
|
|
1937
1953
|
// Unified right panel state
|
|
1938
1954
|
// ────────────────────────────────────────────
|
|
1939
1955
|
type RightPanelTab = 'plan' | 'workstreams' | 'documents' | 'uploads' | 'artifacts' | 'payload' | 'conversation';
|
|
1940
|
-
const [rightPanelTab, setRightPanelTab] = useState<RightPanelTab>(
|
|
1941
|
-
conversationContent || conversationTab ? 'conversation' :
|
|
1956
|
+
const [rightPanelTab, setRightPanelTab] = useState<RightPanelTab | undefined>(
|
|
1957
|
+
conversationContent || conversationTab ? 'conversation' : undefined,
|
|
1942
1958
|
);
|
|
1959
|
+
const defaultRightPanelTab = plans.length > 0 || panelWorkstreams.length === 0 ? 'plan' : 'workstreams';
|
|
1943
1960
|
const [selectedArtifactPath, setSelectedArtifactPath] = useState<string | null>(null);
|
|
1944
1961
|
const [rightPanelWidth, setRightPanelWidth] = useState(400);
|
|
1945
1962
|
const [isRightPanelResizing, setIsRightPanelResizing] = useState(false);
|
|
@@ -2328,13 +2345,32 @@ function ModernAgentConversationInner({
|
|
|
2328
2345
|
if (requestInputId) {
|
|
2329
2346
|
markRequestInputIdAnsweredForSession(agentRunId, requestInputId);
|
|
2330
2347
|
}
|
|
2348
|
+
const restartAndReconnect = async () => {
|
|
2349
|
+
await client.agents.restart(agentRunId);
|
|
2350
|
+
reconnectStream();
|
|
2351
|
+
};
|
|
2331
2352
|
const deliver = (async () => {
|
|
2332
2353
|
await waitForPendingToolApprovalModeChange();
|
|
2333
2354
|
if (isWorkflowTerminalRef.current) {
|
|
2334
|
-
await
|
|
2335
|
-
|
|
2355
|
+
await restartAndReconnect();
|
|
2356
|
+
}
|
|
2357
|
+
try {
|
|
2358
|
+
await sendUserInput();
|
|
2359
|
+
} catch (err: unknown) {
|
|
2360
|
+
// The run can end while this view still shows it running — for example when it
|
|
2361
|
+
// reaches its maximum duration while idle — and the signal then answers 409.
|
|
2362
|
+
// Re-read its status and, if it ended and can be continued, continue it the way
|
|
2363
|
+
// a run already known to be ended is continued above.
|
|
2364
|
+
if (!isConflictError(err)) throw err;
|
|
2365
|
+
const run = await client.agents.retrieve(agentRunId);
|
|
2366
|
+
if (!canContinueWorkflowStatus(run.status, canRestartRef.current)) {
|
|
2367
|
+
// Reconnecting re-reads the status, so the view shows how the run ended.
|
|
2368
|
+
if (isTerminalWorkflowStatus(run.status)) reconnectStream();
|
|
2369
|
+
throw err;
|
|
2370
|
+
}
|
|
2371
|
+
await restartAndReconnect();
|
|
2372
|
+
await sendUserInput();
|
|
2336
2373
|
}
|
|
2337
|
-
await sendUserInput();
|
|
2338
2374
|
markReceived();
|
|
2339
2375
|
})();
|
|
2340
2376
|
|
|
@@ -2903,6 +2939,8 @@ function ModernAgentConversationInner({
|
|
|
2903
2939
|
{composerContext}
|
|
2904
2940
|
<MessageInput
|
|
2905
2941
|
onSend={handleSendMessage}
|
|
2942
|
+
value={composerValue}
|
|
2943
|
+
onValueChange={setComposerValue}
|
|
2906
2944
|
onStop={allowWorkflowControl ? handleStopWorkflow : undefined}
|
|
2907
2945
|
approvalModeSlot={
|
|
2908
2946
|
interactive && toolApprovalMode ? (
|
|
@@ -3081,8 +3119,8 @@ function ModernAgentConversationInner({
|
|
|
3081
3119
|
conversationContent={conversationTab ? conversationAreaJsx : conversationContent}
|
|
3082
3120
|
// Panel control
|
|
3083
3121
|
onClose={handleCloseRightPanel}
|
|
3084
|
-
defaultTab={rightPanelTab}
|
|
3085
|
-
activeTab={rightPanelTab}
|
|
3122
|
+
defaultTab={rightPanelTab ?? defaultRightPanelTab}
|
|
3123
|
+
activeTab={rightPanelTab ?? defaultRightPanelTab}
|
|
3086
3124
|
onTabChange={setRightPanelTab}
|
|
3087
3125
|
/>
|
|
3088
3126
|
</div>
|
|
@@ -55,6 +55,10 @@ function formatTokenCountInK(tokens: number): string {
|
|
|
55
55
|
|
|
56
56
|
interface MessageInputProps {
|
|
57
57
|
onSend: (message: string) => void;
|
|
58
|
+
/** Controlled composer draft. When omitted, the composer manages its own draft. */
|
|
59
|
+
value?: string;
|
|
60
|
+
/** Called whenever the composer draft changes. */
|
|
61
|
+
onValueChange?: (value: string) => void;
|
|
58
62
|
onStop?: () => void;
|
|
59
63
|
disabled?: boolean;
|
|
60
64
|
isSending?: boolean;
|
|
@@ -121,6 +125,8 @@ interface MessageInputProps {
|
|
|
121
125
|
|
|
122
126
|
export default function MessageInput({
|
|
123
127
|
onSend,
|
|
128
|
+
value: controlledValue,
|
|
129
|
+
onValueChange,
|
|
124
130
|
onStop,
|
|
125
131
|
approvalModeSlot,
|
|
126
132
|
mcpSlot,
|
|
@@ -163,7 +169,17 @@ export default function MessageInput({
|
|
|
163
169
|
const resolvedPlaceholder = placeholder ?? t('agent.typeYourMessage');
|
|
164
170
|
const ref = useRef<HTMLTextAreaElement | null>(null);
|
|
165
171
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
166
|
-
const [
|
|
172
|
+
const [uncontrolledValue, setUncontrolledValue] = useState('');
|
|
173
|
+
const value = controlledValue ?? uncontrolledValue;
|
|
174
|
+
const setValue = useCallback(
|
|
175
|
+
(nextValue: string) => {
|
|
176
|
+
if (controlledValue === undefined) {
|
|
177
|
+
setUncontrolledValue(nextValue);
|
|
178
|
+
}
|
|
179
|
+
onValueChange?.(nextValue);
|
|
180
|
+
},
|
|
181
|
+
[controlledValue, onValueChange],
|
|
182
|
+
);
|
|
167
183
|
const [isObjectModalOpen, setIsObjectModalOpen] = useState(false);
|
|
168
184
|
const [isDocSearchOpen, setIsDocSearchOpen] = useState(false);
|
|
169
185
|
const [isDragOver, setIsDragOver] = useState(false);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { act, cleanup, renderHook } from '@testing-library/react';
|
|
2
|
+
import { type AgentMessage, AgentMessageType } from '@vertesia/common';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
4
|
+
import { useAgentPlans } from './useAgentPlans.js';
|
|
5
|
+
|
|
6
|
+
const question: AgentMessage = {
|
|
7
|
+
timestamp: 1,
|
|
8
|
+
workflow_run_id: 'run-1',
|
|
9
|
+
workstream_id: 'main',
|
|
10
|
+
type: AgentMessageType.QUESTION,
|
|
11
|
+
message: 'Help with a task',
|
|
12
|
+
};
|
|
13
|
+
const plan: AgentMessage = {
|
|
14
|
+
...question,
|
|
15
|
+
timestamp: 2,
|
|
16
|
+
type: AgentMessageType.PLAN,
|
|
17
|
+
details: { plan: [{ id: 1, goal: 'Review the task', status: 'pending' }] },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe('useAgentPlans sidebar visibility', () => {
|
|
21
|
+
beforeEach(() => sessionStorage.clear());
|
|
22
|
+
afterEach(cleanup);
|
|
23
|
+
|
|
24
|
+
it('stays closed while history loads and when history has no plan', () => {
|
|
25
|
+
const { result, rerender } = renderHook(({ messages }) => useAgentPlans(messages, true), {
|
|
26
|
+
initialProps: { messages: [] as AgentMessage[] },
|
|
27
|
+
});
|
|
28
|
+
expect(result.current.showSlidingPanel).toBe(false);
|
|
29
|
+
rerender({ messages: [question] });
|
|
30
|
+
expect(result.current.showSlidingPanel).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('opens when a plan arrives and resets for a new empty run', () => {
|
|
34
|
+
const { result, rerender } = renderHook(({ messages }) => useAgentPlans(messages, true), {
|
|
35
|
+
initialProps: { messages: [question] },
|
|
36
|
+
});
|
|
37
|
+
rerender({ messages: [question, plan] });
|
|
38
|
+
expect(result.current.showSlidingPanel).toBe(true);
|
|
39
|
+
expect(result.current.plans).toHaveLength(1);
|
|
40
|
+
rerender({ messages: [] });
|
|
41
|
+
expect(result.current.showSlidingPanel).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('preserves manual opening when subsequent messages have no plan', () => {
|
|
45
|
+
const { result, rerender } = renderHook(({ messages }) => useAgentPlans(messages, true), {
|
|
46
|
+
initialProps: { messages: [question] },
|
|
47
|
+
});
|
|
48
|
+
act(() => result.current.setShowSlidingPanel(true));
|
|
49
|
+
rerender({ messages: [question, { ...question, timestamp: 3, type: AgentMessageType.ANSWER }] });
|
|
50
|
+
expect(result.current.showSlidingPanel).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
});
|