@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.
Files changed (210) hide show
  1. package/lib/core/components/SelectList.d.ts.map +1 -1
  2. package/lib/core/components/SelectList.js +3 -3
  3. package/lib/core/components/SelectList.js.map +1 -1
  4. package/lib/core/components/SidePanel.d.ts.map +1 -1
  5. package/lib/core/components/SidePanel.js +2 -1
  6. package/lib/core/components/SidePanel.js.map +1 -1
  7. package/lib/features/agent/chat/ModernAgentConversation.d.ts.map +1 -1
  8. package/lib/features/agent/chat/ModernAgentConversation.js +51 -17
  9. package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
  10. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.d.ts +5 -1
  11. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.d.ts.map +1 -1
  12. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js +9 -2
  13. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js.map +1 -1
  14. package/lib/features/agent/chat/hooks/useAgentPlans.d.ts +1 -1
  15. package/lib/features/agent/chat/hooks/useAgentPlans.d.ts.map +1 -1
  16. package/lib/features/agent/chat/hooks/useAgentPlans.js +5 -5
  17. package/lib/features/agent/chat/hooks/useAgentPlans.js.map +1 -1
  18. package/lib/features/magic-pdf/GroundedExtractionView.js +3 -3
  19. package/lib/features/magic-pdf/GroundedExtractionView.js.map +1 -1
  20. package/lib/features/oauth/McpConnectionsButton.d.ts +2 -2
  21. package/lib/features/oauth/McpConnectionsButton.d.ts.map +1 -1
  22. package/lib/features/oauth/McpConnectionsButton.js +9 -7
  23. package/lib/features/oauth/McpConnectionsButton.js.map +1 -1
  24. package/lib/features/oauth/McpConnectionsDialog.d.ts.map +1 -1
  25. package/lib/features/oauth/McpConnectionsDialog.js +7 -3
  26. package/lib/features/oauth/McpConnectionsDialog.js.map +1 -1
  27. package/lib/features/oauth/useMcpConnections.d.ts +4 -2
  28. package/lib/features/oauth/useMcpConnections.d.ts.map +1 -1
  29. package/lib/features/oauth/useMcpConnections.js +15 -9
  30. package/lib/features/oauth/useMcpConnections.js.map +1 -1
  31. package/lib/features/store/objects/selection/ObjectsActionContext.d.ts.map +1 -1
  32. package/lib/features/store/objects/selection/ObjectsActionContext.js +2 -0
  33. package/lib/features/store/objects/selection/ObjectsActionContext.js.map +1 -1
  34. package/lib/features/store/objects/selection/SelectionActions.d.ts +4 -2
  35. package/lib/features/store/objects/selection/SelectionActions.d.ts.map +1 -1
  36. package/lib/features/store/objects/selection/SelectionActions.js +6 -3
  37. package/lib/features/store/objects/selection/SelectionActions.js.map +1 -1
  38. package/lib/features/store/objects/selection/actions/RunIntakeAction.d.ts +8 -0
  39. package/lib/features/store/objects/selection/actions/RunIntakeAction.d.ts.map +1 -0
  40. package/lib/features/store/objects/selection/actions/RunIntakeAction.js +65 -0
  41. package/lib/features/store/objects/selection/actions/RunIntakeAction.js.map +1 -0
  42. package/lib/features/store/objects/selection/actions/index.d.ts +1 -0
  43. package/lib/features/store/objects/selection/actions/index.d.ts.map +1 -1
  44. package/lib/features/store/objects/selection/actions/index.js +1 -0
  45. package/lib/features/store/objects/selection/actions/index.js.map +1 -1
  46. package/lib/features/store/types/ContentObjectTypesTable.d.ts.map +1 -1
  47. package/lib/features/store/types/ContentObjectTypesTable.js +11 -3
  48. package/lib/features/store/types/ContentObjectTypesTable.js.map +1 -1
  49. package/lib/features/store/types/IntakePolicyEditor.d.ts.map +1 -1
  50. package/lib/features/store/types/IntakePolicyEditor.js +14 -14
  51. package/lib/features/store/types/IntakePolicyEditor.js.map +1 -1
  52. package/lib/features/store/types/TypeRegistry.d.ts +275 -150
  53. package/lib/features/store/types/TypeRegistry.d.ts.map +1 -1
  54. package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts +275 -150
  55. package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts.map +1 -1
  56. package/lib/i18n/locales/ar.json +28 -7
  57. package/lib/i18n/locales/de.json +24 -7
  58. package/lib/i18n/locales/en.json +24 -7
  59. package/lib/i18n/locales/es.json +24 -7
  60. package/lib/i18n/locales/fr.json +25 -7
  61. package/lib/i18n/locales/it.json +24 -7
  62. package/lib/i18n/locales/ja.json +24 -7
  63. package/lib/i18n/locales/ko.json +24 -7
  64. package/lib/i18n/locales/pt.json +24 -7
  65. package/lib/i18n/locales/ru.json +26 -7
  66. package/lib/i18n/locales/tr.json +24 -7
  67. package/lib/i18n/locales/zh-TW.json +24 -7
  68. package/lib/i18n/locales/zh.json +24 -7
  69. package/lib/shell/apps/AppInstallationProvider.d.ts +4 -4
  70. package/lib/shell/login/SignInIcons.d.ts.map +1 -1
  71. package/lib/shell/login/SignInIcons.js +2 -1
  72. package/lib/shell/login/SignInIcons.js.map +1 -1
  73. package/lib/shell/login/SignInPasswordStep.d.ts +9 -0
  74. package/lib/shell/login/SignInPasswordStep.d.ts.map +1 -0
  75. package/lib/shell/login/SignInPasswordStep.js +58 -0
  76. package/lib/shell/login/SignInPasswordStep.js.map +1 -0
  77. package/lib/shell/login/SignInPrimitives.d.ts +13 -0
  78. package/lib/shell/login/SignInPrimitives.d.ts.map +1 -1
  79. package/lib/shell/login/SignInPrimitives.js +11 -1
  80. package/lib/shell/login/SignInPrimitives.js.map +1 -1
  81. package/lib/shell/login/SignInProvidersStep.d.ts +3 -1
  82. package/lib/shell/login/SignInProvidersStep.d.ts.map +1 -1
  83. package/lib/shell/login/SignInProvidersStep.js +4 -2
  84. package/lib/shell/login/SignInProvidersStep.js.map +1 -1
  85. package/lib/shell/login/SignInReturningStep.d.ts +3 -1
  86. package/lib/shell/login/SignInReturningStep.d.ts.map +1 -1
  87. package/lib/shell/login/SignInReturningStep.js +18 -8
  88. package/lib/shell/login/SignInReturningStep.js.map +1 -1
  89. package/lib/shell/login/SignInTenantStep.d.ts +3 -1
  90. package/lib/shell/login/SignInTenantStep.d.ts.map +1 -1
  91. package/lib/shell/login/SignInTenantStep.js +2 -2
  92. package/lib/shell/login/SignInTenantStep.js.map +1 -1
  93. package/lib/shell/login/SignInWithProviderButton.d.ts +5 -3
  94. package/lib/shell/login/SignInWithProviderButton.d.ts.map +1 -1
  95. package/lib/shell/login/SignInWithProviderButton.js +8 -7
  96. package/lib/shell/login/SignInWithProviderButton.js.map +1 -1
  97. package/lib/shell/login/SigninScreen.d.ts.map +1 -1
  98. package/lib/shell/login/SigninScreen.js +13 -4
  99. package/lib/shell/login/SigninScreen.js.map +1 -1
  100. package/lib/shell/login/signInUtils.d.ts +20 -4
  101. package/lib/shell/login/signInUtils.d.ts.map +1 -1
  102. package/lib/shell/login/signInUtils.js +47 -1
  103. package/lib/shell/login/signInUtils.js.map +1 -1
  104. package/lib/vertesia-ui-core.js +1 -1
  105. package/lib/vertesia-ui-core.js.map +1 -1
  106. package/lib/vertesia-ui-features.js +14 -14
  107. package/lib/vertesia-ui-features.js.map +1 -1
  108. package/lib/vertesia-ui-i18n.BoNE1APG.chunk.js +5 -0
  109. package/lib/vertesia-ui-i18n.BoNE1APG.chunk.js.map +1 -0
  110. package/lib/vertesia-ui-i18n.BoPtiSPb.chunk.js +5 -0
  111. package/lib/vertesia-ui-i18n.BoPtiSPb.chunk.js.map +1 -0
  112. package/lib/vertesia-ui-i18n.C4BBW5v5.chunk.js +5 -0
  113. package/lib/vertesia-ui-i18n.C4BBW5v5.chunk.js.map +1 -0
  114. package/lib/vertesia-ui-i18n.C81wuNkY.chunk.js +5 -0
  115. package/lib/vertesia-ui-i18n.C81wuNkY.chunk.js.map +1 -0
  116. package/lib/vertesia-ui-i18n.C9GipKIH.chunk.js +5 -0
  117. package/lib/vertesia-ui-i18n.C9GipKIH.chunk.js.map +1 -0
  118. package/lib/vertesia-ui-i18n.CWsoS0W9.chunk.js +5 -0
  119. package/lib/vertesia-ui-i18n.CWsoS0W9.chunk.js.map +1 -0
  120. package/lib/vertesia-ui-i18n.Cr9NtzZB.chunk.js +5 -0
  121. package/lib/vertesia-ui-i18n.Cr9NtzZB.chunk.js.map +1 -0
  122. package/lib/vertesia-ui-i18n.DEpkHOD5.chunk.js +5 -0
  123. package/lib/vertesia-ui-i18n.DEpkHOD5.chunk.js.map +1 -0
  124. package/lib/vertesia-ui-i18n.DPXaRKFO.chunk.js +5 -0
  125. package/lib/vertesia-ui-i18n.DPXaRKFO.chunk.js.map +1 -0
  126. package/lib/vertesia-ui-i18n.D_QNm1Xg.chunk.js +5 -0
  127. package/lib/vertesia-ui-i18n.D_QNm1Xg.chunk.js.map +1 -0
  128. package/lib/vertesia-ui-i18n.F26Cc0f7.chunk.js +5 -0
  129. package/lib/vertesia-ui-i18n.F26Cc0f7.chunk.js.map +1 -0
  130. package/lib/vertesia-ui-i18n.YkXIESV_.chunk.js +5 -0
  131. package/lib/vertesia-ui-i18n.YkXIESV_.chunk.js.map +1 -0
  132. package/lib/vertesia-ui-i18n.js +3 -3
  133. package/lib/vertesia-ui-i18n.js.map +1 -1
  134. package/lib/vertesia-ui-shell.js +15 -9
  135. package/lib/vertesia-ui-shell.js.map +1 -1
  136. package/lib/widgets/xml-viewer/hooks/useCollapsible.d.ts +1 -1
  137. package/package.json +7 -7
  138. package/src/core/components/SelectList.tsx +5 -2
  139. package/src/core/components/SidePanel.tsx +65 -60
  140. package/src/features/agent/chat/ModernAgentConversation.test.tsx +150 -1
  141. package/src/features/agent/chat/ModernAgentConversation.tsx +59 -21
  142. package/src/features/agent/chat/ModernAgentOutput/MessageInput.tsx +17 -1
  143. package/src/features/agent/chat/hooks/useAgentPlans.test.ts +52 -0
  144. package/src/features/agent/chat/hooks/useAgentPlans.ts +5 -5
  145. package/src/features/magic-pdf/GroundedExtractionView.tsx +3 -3
  146. package/src/features/oauth/McpConnectionsButton.test.tsx +56 -0
  147. package/src/features/oauth/McpConnectionsButton.tsx +9 -6
  148. package/src/features/oauth/McpConnectionsDialog.tsx +14 -5
  149. package/src/features/oauth/useMcpConnections.test.ts +10 -0
  150. package/src/features/oauth/useMcpConnections.ts +18 -10
  151. package/src/features/store/objects/selection/ObjectsActionContext.tsx +2 -0
  152. package/src/features/store/objects/selection/SelectionActions.tsx +13 -6
  153. package/src/features/store/objects/selection/actions/RunIntakeAction.tsx +69 -0
  154. package/src/features/store/objects/selection/actions/index.ts +1 -0
  155. package/src/features/store/types/ContentObjectTypesTable.tsx +20 -3
  156. package/src/features/store/types/IntakePolicyEditor.tsx +33 -28
  157. package/src/i18n/locales/ar.json +28 -7
  158. package/src/i18n/locales/de.json +24 -7
  159. package/src/i18n/locales/en.json +24 -7
  160. package/src/i18n/locales/es.json +24 -7
  161. package/src/i18n/locales/fr.json +25 -7
  162. package/src/i18n/locales/it.json +24 -7
  163. package/src/i18n/locales/ja.json +24 -7
  164. package/src/i18n/locales/ko.json +24 -7
  165. package/src/i18n/locales/pt.json +24 -7
  166. package/src/i18n/locales/ru.json +26 -7
  167. package/src/i18n/locales/tr.json +24 -7
  168. package/src/i18n/locales/zh-TW.json +24 -7
  169. package/src/i18n/locales/zh.json +24 -7
  170. package/src/shell/login/SignInIcons.tsx +2 -1
  171. package/src/shell/login/SignInPasswordRequired.test.tsx +82 -0
  172. package/src/shell/login/SignInPasswordStep.test.tsx +71 -0
  173. package/src/shell/login/SignInPasswordStep.tsx +103 -0
  174. package/src/shell/login/SignInPrimitives.tsx +53 -1
  175. package/src/shell/login/SignInProvidersStep.tsx +8 -4
  176. package/src/shell/login/SignInReturningStep.tsx +20 -8
  177. package/src/shell/login/SignInTenantStep.tsx +6 -2
  178. package/src/shell/login/SignInWithProviderButton.tsx +11 -7
  179. package/src/shell/login/SigninScreen.tsx +32 -3
  180. package/src/shell/login/signInPassword.test.ts +133 -0
  181. package/src/shell/login/signInUtils.ts +60 -7
  182. package/lib/features/store/objects/selection/actions/StartWorkflowButton.d.ts +0 -7
  183. package/lib/features/store/objects/selection/actions/StartWorkflowButton.d.ts.map +0 -1
  184. package/lib/features/store/objects/selection/actions/StartWorkflowButton.js +0 -51
  185. package/lib/features/store/objects/selection/actions/StartWorkflowButton.js.map +0 -1
  186. package/lib/vertesia-ui-i18n.BfN7iOHb.chunk.js +0 -5
  187. package/lib/vertesia-ui-i18n.BfN7iOHb.chunk.js.map +0 -1
  188. package/lib/vertesia-ui-i18n.BjH2zEZw.chunk.js +0 -5
  189. package/lib/vertesia-ui-i18n.BjH2zEZw.chunk.js.map +0 -1
  190. package/lib/vertesia-ui-i18n.BjSwAZXl.chunk.js +0 -5
  191. package/lib/vertesia-ui-i18n.BjSwAZXl.chunk.js.map +0 -1
  192. package/lib/vertesia-ui-i18n.C7PUm5Au.chunk.js +0 -5
  193. package/lib/vertesia-ui-i18n.C7PUm5Au.chunk.js.map +0 -1
  194. package/lib/vertesia-ui-i18n.CF_1mUS5.chunk.js +0 -5
  195. package/lib/vertesia-ui-i18n.CF_1mUS5.chunk.js.map +0 -1
  196. package/lib/vertesia-ui-i18n.ChfUl3_-.chunk.js +0 -5
  197. package/lib/vertesia-ui-i18n.ChfUl3_-.chunk.js.map +0 -1
  198. package/lib/vertesia-ui-i18n.D-Sk-5d1.chunk.js +0 -5
  199. package/lib/vertesia-ui-i18n.D-Sk-5d1.chunk.js.map +0 -1
  200. package/lib/vertesia-ui-i18n.D4rnu1_S.chunk.js +0 -5
  201. package/lib/vertesia-ui-i18n.D4rnu1_S.chunk.js.map +0 -1
  202. package/lib/vertesia-ui-i18n.DDf0-laK.chunk.js +0 -5
  203. package/lib/vertesia-ui-i18n.DDf0-laK.chunk.js.map +0 -1
  204. package/lib/vertesia-ui-i18n.DN79y06J.chunk.js +0 -5
  205. package/lib/vertesia-ui-i18n.DN79y06J.chunk.js.map +0 -1
  206. package/lib/vertesia-ui-i18n.DWLIciYQ.chunk.js +0 -5
  207. package/lib/vertesia-ui-i18n.DWLIciYQ.chunk.js.map +0 -1
  208. package/lib/vertesia-ui-i18n.DvF_YaAJ.chunk.js +0 -5
  209. package/lib/vertesia-ui-i18n.DvF_YaAJ.chunk.js.map +0 -1
  210. package/src/features/store/objects/selection/actions/StartWorkflowButton.tsx +0 -104
@@ -1,8 +1,8 @@
1
1
  export declare function useCollapsible(level: number): {
2
2
  collapsed: boolean;
3
3
  buttonProps: {
4
- role?: undefined;
5
4
  onClick?: undefined;
5
+ role?: undefined;
6
6
  style?: undefined;
7
7
  } | {
8
8
  onClick: () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertesia/ui",
3
- "version": "1.5.0",
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/fusion-ux": "1.5.0",
96
- "@vertesia/common": "1.5.0",
97
- "@vertesia/json": "1.5.0",
98
- "@vertesia/rich-text": "1.5.0",
99
- "@vertesia/client": "1.5.0"
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": "3485742020066ab1fd3292487cf3a9577b5e4d91",
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 !flex w-full items-center cursor-pointer gap-x-2 hover:bg-muted',
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
- <AnimatePresence>
76
- {isOpen && (
77
- <div className="relative z-20">
78
- {/* Backdrop */}
79
- {backdrop && (
80
- <motion.div
81
- className="fixed inset-0 bg-black/70"
82
- initial={{ opacity: 0 }}
83
- animate={{ opacity: 1 }}
84
- exit={{ opacity: 0 }}
85
- onClick={onClose}
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
- <div className={`fixed inset-y-0 ${positionClass} overflow-hidden`}>
90
- <div className="absolute inset-0 overflow-hidden">
91
- <div
92
- className={`pointer-events-none fixed inset-y-0 ${positionClass} flex max-w-full ${paddingClass}`}
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 className="relative flex h-full">
103
- {/* Drag Handle */}
104
- {resizable && (
105
- // biome-ignore lint/a11y/noStaticElementInteractions: drag handle is pointer-only (no keyboard equivalent for continuous resize); ARIA role omitted to avoid useAriaPropsForRole false positive.
106
- // biome-ignore lint/a11y/useAriaPropsSupportedByRole: aria-label kept for AT users; div has no semantic role because separator/slider would require valuenow/min/max
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
- aria-label="Resize panel"
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
- <Minus className="rotate-90 font-semibold" strokeWidth={4} />
113
- </div>
114
- )}
115
- <div
116
- className={`flex-1 min-w-0 flex flex-col bg-background shadow-xl ${className}`}
117
- >
118
- {/* Sticky header */}
119
- {title && (
120
- <div className="sticky top-0 z-10 bg-background px-2 sm:px-4 py-2 border-b">
121
- <div className="flex items-start justify-between">
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
- </div>
130
- )}
132
+ )}
131
133
 
132
- {/* Scrollable content */}
133
- <div
134
- className={contentClassName ?? 'flex-1 overflow-auto px-2 sm:px-4 py-4'}
135
- >
136
- <div className="w-full h-full flex-1 flex flex-col">{children}</div>
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
- </motion.div>
144
+ </motion.div>
145
+ </div>
141
146
  </div>
142
147
  </div>
143
148
  </div>
144
- </div>
145
- )}
146
- </AnimatePresence>
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('plan');
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, isModal);
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
- const normalizedStatus = effectiveWorkflowStatus?.toUpperCase();
1664
- return (
1665
- normalizedStatus === 'COMPLETED' ||
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
- () => isWorkflowTerminal && effectiveWorkflowStatus?.toUpperCase() !== 'FAILED' && !!onRestart,
1681
- [isWorkflowTerminal, effectiveWorkflowStatus, onRestart],
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' : 'plan',
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 client.agents.restart(agentRunId);
2335
- reconnectStream();
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 [value, setValue] = useState('');
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
+ });