fix(web): show forced sandbox scope + make disabled tooltip tap-friendly

When a SaaS deployment pins every pipeline to a fixed sandbox scope via
system.limitation.force_box_session_id_template, the Sandbox Scope selector was
correctly locked but still displayed the pipeline's stored value (e.g. the
per-chat default), misrepresenting the scope that the runtime actually enforces
on every exec. Coerce the displayed/saved value to the forced template so the
locked selector truthfully shows the active scope (e.g. Global).

Also fix the disabled_tooltip being invisible on touch devices: hover-only Radix
tooltips never open without a pointer, so the explanation of why the field is
locked could not be read on mobile. Wrap the info icon so a tap toggles the
tooltip while desktop hover still works.
This commit is contained in:
RockChinQ
2026-06-08 09:32:55 -04:00
parent 1ee12b68e1
commit 2142e7d735
2 changed files with 52 additions and 18 deletions

View File

@@ -198,6 +198,35 @@ function WebhookUrlField({
);
}
// Hover-only Radix tooltips never open on touch devices (no pointer hover),
// so the ``disabled_tooltip`` explaining why a field is locked was invisible on
// mobile. This wrapper makes the info icon also toggle the tooltip on tap while
// keeping hover behavior on desktop.
function DisabledTooltipIcon({ text }: { text: string }) {
const [open, setOpen] = useState(false);
return (
<TooltipProvider delayDuration={100}>
<Tooltip open={open} onOpenChange={setOpen}>
<TooltipTrigger asChild>
<button
type="button"
aria-label={text}
className="inline-flex shrink-0"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setOpen((v) => !v);
}}
>
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help shrink-0" />
</button>
</TooltipTrigger>
<TooltipContent className="max-w-xs">{text}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
export default function DynamicFormComponent({
itemConfigList,
onSubmit,
@@ -550,18 +579,7 @@ export default function DynamicFormComponent({
? extractI18nObject(config.disabled_tooltip)
: '';
const renderDisabledTooltipIcon = () =>
disabledTooltip ? (
<TooltipProvider delayDuration={100}>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help shrink-0" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">
{disabledTooltip}
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : null;
disabledTooltip ? <DisabledTooltipIcon text={disabledTooltip} /> : null;
// Webhook URL fields are display-only; render outside of form binding
if (config.type === 'webhook-url') {

View File

@@ -427,8 +427,9 @@ export default function PipelineFormComponent({
// 1. Box sandbox is unavailable, or
// 2. the deployment pins all pipelines to a fixed scope via
// ``system.limitation.force_box_session_id_template`` (SaaS).
const boxScopeForced =
!!systemInfo.limitation?.force_box_session_id_template;
const forcedBoxTemplate =
systemInfo.limitation?.force_box_session_id_template || '';
const boxScopeForced = !!forcedBoxTemplate;
const stageSystemContext =
stage.name === 'local-agent'
? {
@@ -437,6 +438,24 @@ export default function PipelineFormComponent({
}
: undefined;
// When the deployment pins every pipeline to a fixed sandbox scope (SaaS
// ``force_box_session_id_template``), the Sandbox Scope selector is locked.
// The runtime already overrides the scope on every exec, but the stored
// pipeline value can be anything (e.g. the per-chat default), which would
// make the locked selector display a scope that is NOT the one actually in
// effect. Coerce the displayed/saved value to the forced template so the UI
// truthfully reflects runtime behavior.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const stageInitialValues: Record<string, any> =
(form.watch(formName) as Record<string, any>)?.[stage.name] || {};
const effectiveInitialValues =
stage.name === 'local-agent' && boxScopeForced
? {
...stageInitialValues,
'box-session-id-template': forcedBoxTemplate,
}
: stageInitialValues;
return (
<Card key={stage.name}>
<CardHeader>
@@ -450,10 +469,7 @@ export default function PipelineFormComponent({
<CardContent className="space-y-6">
<DynamicFormComponent
itemConfigList={stage.config}
initialValues={
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(form.watch(formName) as Record<string, any>)?.[stage.name] || {}
}
initialValues={effectiveInitialValues}
onSubmit={(values) => {
handleDynamicFormEmit(formName, stage.name, values);
}}