@jini-ai/ui 0.6.15-rc.2 → 0.6.15
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/dist/browser/useDismissOnOutsideOrEscape.js +1 -1
- package/dist/browser/useGlobalKeydown.js +2 -2
- package/dist/browser/useModalWindowDragGuard.js +1 -1
- package/dist/core/theme/css-name.d.ts +3 -0
- package/dist/core/theme/css-name.js +5 -0
- package/dist/core/theme/stylesheet.js +2 -1
- package/dist/core/theme/validation.js +15 -4
- package/dist/features/about/rules.js +2 -1
- package/dist/features/admin-widgets/components/InfoTip.hooks.js +22 -12
- package/dist/features/admin-widgets/components/Lightbox/Lightbox.hooks.js +1 -1
- package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.hooks.js +1 -1
- package/dist/features/admin-widgets/components/SeeMore/SeeMore.hooks.js +2 -2
- package/dist/features/admin-widgets/components/SeeMore/SeeMore.js +4 -2
- package/dist/features/admin-widgets/components/Select/Select.hooks.js +2 -1
- package/dist/features/admin-widgets/components/Select/Select.js +2 -1
- package/dist/features/admin-widgets/components/SuggestInput/SuggestInput.js +4 -2
- package/dist/features/admin-widgets/components/useDebouncedHtmlPreview.hooks.js +3 -2
- package/dist/features/asset-grid/react/components/AssetGrid.js +8 -3
- package/dist/features/asset-grid/react/hooks/useAssetGridKeyboardShortcuts.js +7 -6
- package/dist/features/asset-grid/react/hooks/useAssetGridLiveUpdates.js +8 -10
- package/dist/features/asset-grid/react/hooks/useRubberBandDrag.js +1 -1
- package/dist/features/asset-tree-browser/react/components/AssetTreeFileRow.js +6 -8
- package/dist/features/asset-tree-browser/react/hooks/useAssetTreeRowMenu.js +1 -1
- package/dist/features/asset-tree-browser/rules.js +1 -3
- package/dist/features/browser-chrome/rules.js +10 -8
- package/dist/features/charts/browser.js +2 -1
- package/dist/features/charts/react.js +3 -1
- package/dist/features/connectors/ConnectorsBrowser.js +3 -1
- package/dist/features/connectors/hooks/useConnectorAuthorization.js +1 -1
- package/dist/features/connectors/hooks/useConnectorCatalog.js +2 -2
- package/dist/features/connectors/rules.js +10 -14
- package/dist/features/execution/react/components/AgentDiagnosticRow.js +2 -0
- package/dist/features/execution/react/components/ByokProviderForm.js +1 -3
- package/dist/features/execution/react/components/LocalCliAgentSettings.js +2 -1
- package/dist/features/execution/react/hooks/use-byok-field-validation.hooks.js +6 -2
- package/dist/features/execution/react/hooks/useExecutionTab.js +9 -5
- package/dist/features/execution/react/hooks/useLocalCliAgentCard.js +3 -6
- package/dist/features/execution/react/hooks/useReasoningControl.js +3 -3
- package/dist/features/execution/rules.js +23 -18
- package/dist/features/external-mcp/react/components/ExternalMcpTab.js +10 -1
- package/dist/features/file-dropzone/rules.js +3 -2
- package/dist/features/html-editor/canvas-embed-placeholders.js +2 -3
- package/dist/features/html-editor/css.js +7 -5
- package/dist/features/html-editor/editor-blocks.js +60 -6
- package/dist/features/html-editor/protected-elements.js +5 -3
- package/dist/features/html-editor/react/hooks/useInteractiveHtmlEditor.js +3 -3
- package/dist/features/html-editor/source-splice.js +6 -4
- package/dist/features/iframe-pool/react/components/IframeKeepAliveProvider.js +5 -5
- package/dist/features/iframe-pool/react/dom-sync.js +2 -2
- package/dist/features/integrations/react/components/IntegrationsTab.js +3 -1
- package/dist/features/integrations/rules.js +6 -4
- package/dist/features/interactive-ui/providers/shadcn/data-table.js +10 -5
- package/dist/features/interactive-ui/table-data.js +1 -1
- package/dist/features/lexical-rich-text-editor/deserialize.js +2 -2
- package/dist/features/lexical-rich-text-editor/mention-parser.js +4 -4
- package/dist/features/lexical-rich-text-editor/react/components/RichTextInput.js +2 -1
- package/dist/features/lexical-rich-text-editor/react/hooks/useCaretFloatingLayerPosition.js +1 -1
- package/dist/features/lexical-rich-text-editor/react/hooks/useMentionColorStamping.js +1 -1
- package/dist/features/lexical-rich-text-editor/serialize.js +13 -10
- package/dist/features/list-detail-panel/react/components/ListDetailPanel.js +1 -1
- package/dist/features/mcp-ui/confirmation-store.js +3 -6
- package/dist/features/mcp-ui/surfaces/confirmation.d.ts +5 -0
- package/dist/features/mcp-ui/surfaces/confirmation.js +1 -0
- package/dist/features/mcp-ui/surfaces/document.d.ts +3 -15
- package/dist/features/mcp-ui/surfaces/document.js +37 -6
- package/dist/features/mcp-ui/surfaces/fields.js +2 -0
- package/dist/features/mcp-ui/surfaces/host-theme.js +2 -1
- package/dist/features/mcp-ui/surfaces/index.d.ts +1 -0
- package/dist/features/mcp-ui/surfaces/index.js +1 -0
- package/dist/features/mcp-ui/surfaces/outcome.js +1 -1
- package/dist/features/mcp-ui/surfaces/select.js +2 -1
- package/dist/features/mcp-ui/surfaces/structured-details.d.ts +40 -0
- package/dist/features/mcp-ui/surfaces/structured-details.js +159 -0
- package/dist/features/mcp-ui/surfaces/text-input.js +1 -1
- package/dist/features/mcp-ui/surfaces/tool-approval.d.ts +21 -0
- package/dist/features/mcp-ui/surfaces/tool-approval.js +41 -0
- package/dist/features/media-providers/react/hooks/useMediaProvidersTab.js +2 -1
- package/dist/features/memory/async-commit-guard.js +4 -1
- package/dist/features/memory/formatters.js +5 -1
- package/dist/features/memory/react/components/MemoryConnectedPanel.js +3 -1
- package/dist/features/memory/react/components/MemoryEntryCard.js +1 -1
- package/dist/features/memory/react/components/MemorySettingsPanel.js +3 -1
- package/dist/features/memory/react/hooks/useMemoryConfig.hooks.js +12 -10
- package/dist/features/memory/react/hooks/useMemoryConnectors.hooks.js +2 -5
- package/dist/features/memory/react/hooks/useMemoryConnectors.rules.js +10 -9
- package/dist/features/memory/react/hooks/useMemoryEntries.hooks.js +4 -2
- package/dist/features/memory/react/hooks/useMemoryFlash.hooks.js +1 -1
- package/dist/features/memory/rules.js +2 -1
- package/dist/features/panel-kit/fetch-query/adapter.react.js +3 -2
- package/dist/features/panel-kit/fetch-query/adapter.tanstack.js +3 -2
- package/dist/features/panel-kit/fetch-query/binding.react.js +2 -1
- package/dist/features/panel-kit/helpers/standing-draft-recovery-message.js +13 -4
- package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.js +1 -1
- package/dist/features/panel-kit/hooks/use-focus-trap.hooks.js +1 -1
- package/dist/features/panel-kit/hooks/use-settings-slice.hooks.js +14 -10
- package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.js +2 -2
- package/dist/features/progress-card/components/ProgressCard.d.ts +1 -1
- package/dist/features/progress-card/reference-adapters.js +23 -13
- package/dist/features/progress-card/rules.d.ts +0 -1
- package/dist/features/progress-card/rules.js +14 -20
- package/dist/features/progress-card/types.d.ts +1 -1
- package/dist/features/project-locations/dependencies.js +6 -1
- package/dist/features/project-locations/react/components/ProjectLocationsTab.js +2 -1
- package/dist/features/project-locations/rules.js +2 -1
- package/dist/features/resource-dashboard/react/components/ResourceBoardToolbar.js +8 -1
- package/dist/features/resource-dashboard/react/components/ResourceCardParts.js +2 -1
- package/dist/features/resource-dashboard/react/components/ResourceRowListItem.js +3 -1
- package/dist/features/resource-dashboard/react/components/ResourceRowListItemParts.js +2 -1
- package/dist/features/resource-dashboard/react/components/ResourceRowListView.js +1 -1
- package/dist/features/resource-dashboard/react/hooks/useResourceBoard.js +1 -1
- package/dist/features/schedule-picker/rules.js +13 -7
- package/dist/features/sketch-editor/constants.d.ts +0 -6
- package/dist/features/sketch-editor/constants.js +17 -15
- package/dist/features/sketch-editor/dom.js +4 -7
- package/dist/features/sketch-editor/react/components/SketchMainMenu.js +2 -1
- package/dist/features/sketch-editor/react/components/SketchSaveStateBadge.js +2 -1
- package/dist/features/sketch-editor/react/hooks/useSketchDomEnhancements.js +1 -1
- package/dist/features/sketch-editor/rules.js +13 -12
- package/dist/features/skills/dependencies.js +9 -4
- package/dist/features/skills/react/components/SkillRow.js +2 -1
- package/dist/features/skills/react/components/SkillsTab.js +2 -1
- package/dist/features/skills/rules.js +3 -3
- package/dist/features/source-config-list/agent-handles.js +4 -3
- package/dist/features/source-config-list/react/components/SourceConfigItemCard.js +3 -1
- package/dist/features/tab-strip/react/components/TabBar.hooks.js +4 -2
- package/dist/features/version-manager/react/components/VersionPreviewFrame.js +4 -1
- package/dist/features/version-manager/react/components/VersionSidebar.js +8 -6
- package/dist/features/version-manager/react/hooks/usePreviewCanvasSize.js +1 -1
- package/dist/features/version-manager/react/hooks/useVersionManager.js +5 -7
- package/dist/features/version-manager/rules.js +3 -5
- package/dist/features/viewer-shell/react/components/CommentSidePanel.js +2 -1
- package/dist/features/viewer-shell/react/components/SegmentedToggle.js +2 -1
- package/dist/features/viewer-shell/react/components/ViewerShell.js +2 -1
- package/dist/features/viewer-shell/rules.js +8 -7
- package/dist/react/components/BrandLogo.js +10 -2
- package/dist/react/components/CustomSelect.js +1 -1
- package/dist/react/components/EditorIcon.js +17 -14
- package/dist/react/components/Loading.js +2 -1
- package/dist/react/components/OnboardingChipField.js +5 -1
- package/dist/react/components/OnboardingDropdown.js +6 -4
- package/dist/react/components/PaletteTweaks.js +7 -6
- package/dist/react/components/PillButton.js +2 -1
- package/dist/react/components/PopoverMenu.js +2 -1
- package/dist/react/components/RemixIcon.js +2 -1
- package/dist/react/components/SecretInput.hooks.d.ts +2 -25
- package/dist/react/components/SecretInput.hooks.js +2 -24
- package/dist/react/components/SecretInput.js +5 -4
- package/dist/react/components/Toast.js +4 -2
- package/dist/react/components/TooltipLayer.js +4 -1
- package/dist/react/components/WorkingDirPicker.js +5 -4
- package/dist/react/components/remixicon-font-recovery.js +9 -1
- package/dist/react/components/select-popover.js +2 -2
- package/dist/react/hooks/useBrandFonts.js +1 -1
- package/dist/react/hooks/useEdgeAutoScroll.js +1 -1
- package/dist/react/hooks/useInView.js +9 -9
- package/dist/react/hooks/useResizableSplitPane.js +12 -20
- package/dist/react/mcp-ui/useHostSurfaceTokens.js +1 -1
- package/dist/renderers/annotation-canvas/react/components/AnnotationCanvas.js +7 -1
- package/dist/renderers/annotation-canvas/react/hooks/useAnnotationCanvas.controller.js +4 -2
- package/dist/renderers/annotation-canvas/react/hooks/useAnnotationCanvas.js +9 -5
- package/dist/renderers/annotation-canvas/rules.js +9 -13
- package/dist/renderers/annotation-canvas/theme.js +5 -2
- package/dist/renderers/renderers/index.js +1 -1
- package/dist/renderers/renderers/markdown.js +4 -6
- package/dist/theme/apply.js +7 -3
- package/dist/utils/class-names.d.ts +3 -0
- package/dist/utils/class-names.js +5 -0
- package/dist/utils/design-md.js +3 -3
- package/dist/utils/ime-composing.d.ts +1 -1
- package/dist/utils/ime-composing.js +1 -2
- package/dist/utils/localized-url.js +2 -11
- package/dist/utils/markdown-scroll-sync.js +1 -1
- package/dist/utils/omit-keys.d.ts +3 -0
- package/dist/utils/omit-keys.js +8 -0
- package/dist/utils/polygon-selection.js +1 -1
- package/dist/utils/sse.js +3 -0
- package/dist/utils/trim-chars.d.ts +13 -0
- package/dist/utils/trim-chars.js +30 -0
- package/dist/utils/unhandled-case.d.ts +10 -0
- package/dist/utils/unhandled-case.js +12 -0
- package/dist/utils/unit-bezier.js +1 -3
- package/dist/utils/zip.js +6 -10
- package/package.json +10 -9
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { omitKeys } from '../../utils/omit-keys.js';
|
|
1
2
|
/** Restricts the catalog to a single provider tab's `match` predicate, defaulting to the first tab. */
|
|
2
3
|
export function scopeConnectorsToProvider(connectors, providerTabs, selectedProviderId) {
|
|
3
4
|
const tab = providerTabs.find((p) => p.id === selectedProviderId) ?? providerTabs[0];
|
|
@@ -176,8 +177,7 @@ export function applyConnectorStatuses(current, statuses) {
|
|
|
176
177
|
const next = statuses[connector.id];
|
|
177
178
|
if (!next)
|
|
178
179
|
return connector;
|
|
179
|
-
|
|
180
|
-
return { ...base, ...next };
|
|
180
|
+
return { ...omitKeys(connector, ['accountLabel', 'lastError']), ...next };
|
|
181
181
|
});
|
|
182
182
|
}
|
|
183
183
|
export function getConnectorDisplayToolCount(connector) {
|
|
@@ -214,8 +214,7 @@ export function mergeConnectorToolPreview(current, next, append) {
|
|
|
214
214
|
};
|
|
215
215
|
if (next.toolsNextCursor !== undefined)
|
|
216
216
|
return { ...merged, toolsNextCursor: next.toolsNextCursor };
|
|
217
|
-
|
|
218
|
-
return withoutCursor;
|
|
217
|
+
return omitKeys(merged, ['toolsNextCursor']);
|
|
219
218
|
}
|
|
220
219
|
export function mergeConnectorActionResult(current, next) {
|
|
221
220
|
const toolCount = next.toolCount ?? current.toolCount;
|
|
@@ -355,17 +354,14 @@ export function toolsBadgeTranslation(count) {
|
|
|
355
354
|
return { key: '1 tool' };
|
|
356
355
|
return { key: '{count} tools', vars: { count } };
|
|
357
356
|
}
|
|
357
|
+
const STATUS_LABELS = {
|
|
358
|
+
available: 'Available',
|
|
359
|
+
connected: 'Connected',
|
|
360
|
+
error: 'Error',
|
|
361
|
+
disabled: 'Disabled',
|
|
362
|
+
};
|
|
358
363
|
export function statusLabel(status) {
|
|
359
|
-
|
|
360
|
-
case 'available':
|
|
361
|
-
return 'Available';
|
|
362
|
-
case 'connected':
|
|
363
|
-
return 'Connected';
|
|
364
|
-
case 'error':
|
|
365
|
-
return 'Error';
|
|
366
|
-
case 'disabled':
|
|
367
|
-
return 'Disabled';
|
|
368
|
-
}
|
|
364
|
+
return STATUS_LABELS[status];
|
|
369
365
|
}
|
|
370
366
|
export function defaultCategoryLabel(category) {
|
|
371
367
|
return category;
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useT } from '../../../i18n/index.js';
|
|
3
3
|
import { agentDiagnosticTooltip } from '../../rules.js';
|
|
4
4
|
import { Icon } from '../../../../react/components/Icon.js';
|
|
5
|
+
import { unhandledCase } from '../../../../utils/unhandled-case.js';
|
|
5
6
|
/**
|
|
6
7
|
* Maps one typed fix intent to a concrete icon button. `'setEnv'`/
|
|
7
8
|
* `'clearEnv'`/`'launchOAuth'` intentionally resolve to no button — same as
|
|
@@ -31,6 +32,7 @@ function resolveAction(intent, handlers, labels) {
|
|
|
31
32
|
case 'launchOAuth':
|
|
32
33
|
return null;
|
|
33
34
|
}
|
|
35
|
+
return unhandledCase(intent);
|
|
34
36
|
}
|
|
35
37
|
/**
|
|
36
38
|
* Presents a single agent diagnostic as "one-line reason + fix button(s)".
|
|
@@ -133,9 +133,7 @@ function ByokConnectionResult({ connectionTest, canTestConnection }) {
|
|
|
133
133
|
return null;
|
|
134
134
|
if (connectionTest.status !== 'ok' && connectionTest.status !== 'error')
|
|
135
135
|
return null;
|
|
136
|
-
return (_jsx("div", { className: "jini-byok-test-row", children: _jsx("span", { className: `jini-byok-test-status is-${connectionTest.status}`, role: connectionTest.status === 'error' ? 'alert' : 'status', children: connectionTest.
|
|
137
|
-
? (connectionTest.message !== undefined ? t(connectionTest.message) : t('Connection succeeded'))
|
|
138
|
-
: t(connectionTest.message) }) }));
|
|
136
|
+
return (_jsx("div", { className: "jini-byok-test-row", children: _jsx("span", { className: `jini-byok-test-status is-${connectionTest.status}`, role: connectionTest.status === 'error' ? 'alert' : 'status', children: t(connectionTest.message ?? 'Connection succeeded') }) }));
|
|
139
137
|
}
|
|
140
138
|
/** Discovery failure, as an inline hint below the Model row. Renders nothing otherwise. */
|
|
141
139
|
function ByokDiscoveryError({ modelDiscovery }) {
|
|
@@ -20,7 +20,8 @@ function LocalCliAgentModelControls({ agent, card, onModelChange, agentHandle })
|
|
|
20
20
|
const { reasoning, source, sourceLabel, sourceHint } = card;
|
|
21
21
|
const { hasModels, hasReasoning, reasoningOptions, reasoningValue, reasoningDisabled, allowCustomModel, customActive, modelChoices, selectValue, customModelInputValue, } = reasoning;
|
|
22
22
|
const additionalOptions = allowCustomModel ? [{ value: CUSTOM_MODEL_SENTINEL, label: t('Custom…') }] : undefined;
|
|
23
|
-
|
|
23
|
+
const modelHandleProps = agentHandle ? { agentHandle: agentSubHandle({ base: agentHandle, action: 'model' }) } : {};
|
|
24
|
+
return (_jsxs("div", { className: "jini-agent-settings-row", children: [hasModels ? (_jsxs("div", { className: "jini-agent-settings-field jini-agent-settings-model", children: [_jsxs("div", { className: "jini-agent-settings-label", children: [_jsx("span", { className: "jini-field-label", children: t('Model') }), sourceLabel ? (_jsx("span", { className: `jini-agent-model-source ${source}`, children: sourceLabel })) : null] }), _jsx(SearchableModelSelect, { className: "jini-input jini-agent-model-select", ariaLabel: t('Model'), searchPlaceholder: t('Search models'), testId: `jini-agent-model-${agent.id}`, searchInputTestId: `jini-agent-model-search-${agent.id}`, ...modelHandleProps, value: selectValue, models: modelChoices, additionalOptions: additionalOptions, onChange: card.selectModel }), sourceHint ? _jsx("p", { className: "jini-field-hint", children: sourceHint }) : null] })) : null, customActive ? (_jsxs("label", { className: "jini-agent-settings-field", children: [_jsx("span", { className: "jini-field-label", children: t('Custom model id') }), _jsx("input", { className: "jini-input jini-agent-settings-custom", type: "text", "data-testid": `jini-agent-model-custom-${agent.id}`, value: customModelInputValue, placeholder: t('e.g. my-fine-tuned-model'), spellCheck: false, onChange: (event) => onModelChange(agent.id, event.target.value.trim()), ...agentHandleProps({}, { base: agentHandle, action: 'custom-model', role: 'field', label: t('Custom model id') }) })] })) : null, hasReasoning ? (_jsxs("label", { className: "jini-agent-settings-field", children: [_jsx("span", { className: "jini-field-label", children: t('Reasoning effort') }), _jsx("select", { className: "jini-input jini-agent-settings-reasoning", "data-testid": `jini-agent-reasoning-${agent.id}`, value: reasoningValue, disabled: reasoningDisabled, onChange: (event) => card.selectReasoning(event.target.value), ...agentHandleProps({}, { base: agentHandle, action: 'reasoning', role: 'field', label: t('Reasoning effort') }), children: reasoningOptions.map((option) => (_jsx("option", { value: option.id, children: option.label }, option.id))) })] })) : null] }));
|
|
24
25
|
}
|
|
25
26
|
/** Probe evidence and repair actions stay together beneath the model controls. */
|
|
26
27
|
function LocalCliAgentFeedback({ agent, card, onEnvChange }) {
|
|
@@ -14,8 +14,12 @@ export function useByokFieldValidation(required, optional = {}) {
|
|
|
14
14
|
return {
|
|
15
15
|
missing,
|
|
16
16
|
baseUrlInvalid,
|
|
17
|
-
isMissing: ({ field }, _options = {}) =>
|
|
18
|
-
(
|
|
17
|
+
isMissing: ({ field }, _options = {}) => {
|
|
18
|
+
const invalid = missing.has(field) || (field === 'baseUrl' && baseUrlInvalid);
|
|
19
|
+
if (!invalid || !optional.deferMissingFeedback)
|
|
20
|
+
return invalid;
|
|
21
|
+
return touched.providerId === config.providerId && touched.fields.has(field);
|
|
22
|
+
},
|
|
19
23
|
markTouched: ({ field }, _options = {}) => {
|
|
20
24
|
setTouched(current => ({
|
|
21
25
|
providerId: config.providerId,
|
|
@@ -48,7 +48,8 @@ export function useExecutionTab({ port, autoDetect = true, describeProbeError =
|
|
|
48
48
|
}, []);
|
|
49
49
|
const runDetection = useCallback((detect) => {
|
|
50
50
|
clearTimeout(detectionTimer.current);
|
|
51
|
-
|
|
51
|
+
detectionTicket.current += 1;
|
|
52
|
+
const ticket = detectionTicket.current;
|
|
52
53
|
const isCurrent = () => alive.current && detectionTicket.current === ticket;
|
|
53
54
|
setScan({ status: 'scanning' });
|
|
54
55
|
detect().then((found) => {
|
|
@@ -60,7 +61,7 @@ export function useExecutionTab({ port, autoDetect = true, describeProbeError =
|
|
|
60
61
|
if (found.some(agent => agent.discoveryPending)) {
|
|
61
62
|
detectionTimer.current = setTimeout(() => {
|
|
62
63
|
if (isCurrent())
|
|
63
|
-
runDetection(
|
|
64
|
+
runDetection(port.detectLocalAgents.bind(port));
|
|
64
65
|
}, 1000);
|
|
65
66
|
}
|
|
66
67
|
}, (error) => {
|
|
@@ -81,7 +82,8 @@ export function useExecutionTab({ port, autoDetect = true, describeProbeError =
|
|
|
81
82
|
runDetection(() => rescanFn.call(port));
|
|
82
83
|
}, [port, runDetection]);
|
|
83
84
|
const testConnection = useCallback((config) => {
|
|
84
|
-
|
|
85
|
+
connectionTestTicket.current += 1;
|
|
86
|
+
const ticket = connectionTestTicket.current;
|
|
85
87
|
const isCurrent = () => alive.current && connectionTestTicket.current === ticket;
|
|
86
88
|
setConnectionTest({ status: 'testing' });
|
|
87
89
|
port.testConnection(config).then((result) => {
|
|
@@ -100,7 +102,8 @@ export function useExecutionTab({ port, autoDetect = true, describeProbeError =
|
|
|
100
102
|
const run = port.testAgent;
|
|
101
103
|
if (!run)
|
|
102
104
|
return;
|
|
103
|
-
|
|
105
|
+
agentTestTicket.current += 1;
|
|
106
|
+
const ticket = agentTestTicket.current;
|
|
104
107
|
const isCurrent = () => alive.current && agentTestTicket.current === ticket;
|
|
105
108
|
setAgentTest({ status: 'testing', agentId });
|
|
106
109
|
run.call(port, agentId, model).then((result) => {
|
|
@@ -128,7 +131,8 @@ export function useExecutionTab({ port, autoDetect = true, describeProbeError =
|
|
|
128
131
|
const listModels = port.listModels;
|
|
129
132
|
if (!listModels)
|
|
130
133
|
return;
|
|
131
|
-
|
|
134
|
+
modelDiscoveryTicket.current += 1;
|
|
135
|
+
const ticket = modelDiscoveryTicket.current;
|
|
132
136
|
const isCurrent = () => alive.current && modelDiscoveryTicket.current === ticket;
|
|
133
137
|
setModelDiscovery({ status: 'loading' });
|
|
134
138
|
listModels.call(port, config).then((found) => {
|
|
@@ -53,14 +53,11 @@ export function useLocalCliAgentCard({ agent, config, agentTest, cliEnvFields, o
|
|
|
53
53
|
}
|
|
54
54
|
onReasoningChange(agent.id, nextValue);
|
|
55
55
|
};
|
|
56
|
-
const hasSettings = reasoning.hasModels ||
|
|
57
|
-
|
|
58
|
-
Boolean(onTest) ||
|
|
59
|
-
result !== null ||
|
|
60
|
-
cliEnvFieldsForAgent(cliEnvFields, agent.id).length > 0;
|
|
56
|
+
const hasSettings = reasoning.hasModels || reasoning.hasReasoning || Boolean(onTest) || result !== null;
|
|
57
|
+
const hasEnvironmentSettings = cliEnvFieldsForAgent(cliEnvFields, agent.id).length > 0;
|
|
61
58
|
return {
|
|
62
59
|
reasoning,
|
|
63
|
-
hasSettings,
|
|
60
|
+
hasSettings: hasSettings || hasEnvironmentSettings,
|
|
64
61
|
source,
|
|
65
62
|
sourceLabel,
|
|
66
63
|
sourceHint,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect } from 'react';
|
|
2
2
|
import { CUSTOM_MODEL_SENTINEL } from '../../constants.js';
|
|
3
3
|
import { modelIdForReasoningLevel, pickerModelOptions, reasoningModelGroupFor, reasoningModelGroups, selectedAgentModel, selectedAgentReasoning, shouldShowCustomModelInput, splitReasoningModelId, } from '../../rules.js';
|
|
4
|
-
export function useReasoningControl(optionsOrAgent, maybeConfig, maybeExplicitCustomMode) {
|
|
4
|
+
export function useReasoningControl(optionsOrAgent, maybeConfig, maybeExplicitCustomMode = false) {
|
|
5
5
|
const { agent, config, explicitCustomMode = false, onReasoningChange } = reasoningControlOptions(optionsOrAgent, maybeConfig, maybeExplicitCustomMode);
|
|
6
6
|
const models = agent.models ?? [];
|
|
7
7
|
const hasModels = models.length > 0;
|
|
@@ -23,7 +23,7 @@ export function useReasoningControl(optionsOrAgent, maybeConfig, maybeExplicitCu
|
|
|
23
23
|
onReasoningChange('');
|
|
24
24
|
// `reasoningOptionIdsKey` stands in for `reasoningOptions` (a fresh array every render) so this
|
|
25
25
|
// only re-runs when the narrowed vocabulary actually changes, not on every unrelated re-render.
|
|
26
|
-
//
|
|
26
|
+
// Dependency list intentionally follows the lifecycle rationale above.
|
|
27
27
|
}, [flatReasoningOptions.length, onReasoningChange, config.reasoningByAgentId, agent.id, reasoningOptionIdsKey]);
|
|
28
28
|
const { allowCustomModel, knownModelIds, customActive } = customModelState({ agent, modelGroups, models, resolvedModel, explicitCustomMode });
|
|
29
29
|
// A model-suffix agent picks a BASE model here; the effort control below supplies the rest of the
|
|
@@ -83,7 +83,7 @@ export function useReasoningControl(optionsOrAgent, maybeConfig, maybeExplicitCu
|
|
|
83
83
|
}
|
|
84
84
|
/** The two call shapes (options object, or the older positional `agent, config, explicitCustomMode`)
|
|
85
85
|
* as one options object. */
|
|
86
|
-
function reasoningControlOptions(optionsOrAgent, maybeConfig, maybeExplicitCustomMode) {
|
|
86
|
+
function reasoningControlOptions(optionsOrAgent, maybeConfig, maybeExplicitCustomMode = false) {
|
|
87
87
|
if ('agent' in optionsOrAgent && 'config' in optionsOrAgent)
|
|
88
88
|
return optionsOrAgent;
|
|
89
89
|
return {
|
|
@@ -118,10 +118,10 @@ function foreignKeyOwner(key, preset, presets) {
|
|
|
118
118
|
const prefix = candidate.apiKeyPrefix;
|
|
119
119
|
if (!prefix || candidate.custom || candidate.id === preset.id)
|
|
120
120
|
continue;
|
|
121
|
-
if (prefix.length
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
if (prefix.length > bestClaim && key.startsWith(prefix)) {
|
|
122
|
+
owner = candidate;
|
|
123
|
+
bestClaim = prefix.length;
|
|
124
|
+
}
|
|
125
125
|
}
|
|
126
126
|
return owner;
|
|
127
127
|
}
|
|
@@ -328,9 +328,9 @@ export function cleanAgentVersionLabel(label, version) {
|
|
|
328
328
|
* binary path, so the card stays one line without discarding either.
|
|
329
329
|
*/
|
|
330
330
|
export function agentMetaLabel(agent, labels) {
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
331
|
+
const authUnresolved = agent.authStatus === 'missing' || agent.authStatus === 'unknown';
|
|
332
|
+
const pathTitle = agent.path ?? '';
|
|
333
|
+
const title = authUnresolved ? (agent.authMessage ?? pathTitle) : pathTitle;
|
|
334
334
|
if (!agent.installed)
|
|
335
335
|
return { text: labels.notInstalled, title };
|
|
336
336
|
if (agent.authStatus === 'missing')
|
|
@@ -505,10 +505,11 @@ export function pickerModelOptions({ models }, _optional = {}) {
|
|
|
505
505
|
const existing = choices.get(id);
|
|
506
506
|
if (!existing) {
|
|
507
507
|
choices.set(id, option);
|
|
508
|
+
continue;
|
|
508
509
|
}
|
|
509
|
-
|
|
510
|
+
// A later duplicate only upgrades an entry whose label was just its id.
|
|
511
|
+
if (existing.label === id && label !== id)
|
|
510
512
|
choices.set(id, { ...existing, label });
|
|
511
|
-
}
|
|
512
513
|
}
|
|
513
514
|
return [...choices.values()];
|
|
514
515
|
}
|
|
@@ -578,21 +579,25 @@ export function splitReasoningModelId(modelId, levelIds) {
|
|
|
578
579
|
* has no level, so nothing is stripped, and a vendor label that happens to end in an unrelated
|
|
579
580
|
* parenthetical keeps it.
|
|
580
581
|
*/
|
|
581
|
-
/** `Head (Inner)` with optional surrounding whitespace; group 1 is the head, group 2 the qualifier. */
|
|
582
|
-
const TRAILING_PARENTHETICAL = /^(.*?)\s*\(([^()]*)\)\s*$/;
|
|
583
582
|
function baseLabelForLevel(label, level) {
|
|
584
583
|
if (!level)
|
|
585
584
|
return label;
|
|
586
|
-
|
|
585
|
+
const trimmed = label.trimEnd();
|
|
586
|
+
if (!trimmed.endsWith(')'))
|
|
587
|
+
return label;
|
|
588
|
+
const open = trimmed.lastIndexOf('(');
|
|
589
|
+
if (open < 0)
|
|
590
|
+
return label;
|
|
591
|
+
const head = trimmed.slice(0, open).trimEnd();
|
|
592
|
+
const qualifier = trimmed.slice(open + 1, -1);
|
|
593
|
+
// A matched terminal group always supplies both parts; the old capture fallbacks were unreachable.
|
|
594
|
+
// The original head's dot did not span line breaks, and the qualifier excluded parentheses.
|
|
595
|
+
if (/[\r\n\u2028\u2029]/.test(head) || qualifier.includes(')'))
|
|
587
596
|
return label;
|
|
588
|
-
|
|
589
|
-
// `replace` (which yields a plain string) replaces the old `match[n] ?? ''` fallbacks, which no
|
|
590
|
-
// matching label could ever reach.
|
|
591
|
-
const inner = label.replace(TRAILING_PARENTHETICAL, '$2').trim().toLowerCase();
|
|
597
|
+
const inner = qualifier.trim().toLowerCase();
|
|
592
598
|
if (inner !== level.id.toLowerCase() && inner !== level.label.toLowerCase())
|
|
593
599
|
return label;
|
|
594
|
-
|
|
595
|
-
return stripped || label;
|
|
600
|
+
return head.trim() || label;
|
|
596
601
|
}
|
|
597
602
|
/**
|
|
598
603
|
* Groups an agent's model list by base model, recording for each base the effort levels it
|
|
@@ -48,7 +48,16 @@ export function ExternalMcpTab({ dependencies, fieldSpecs = MCP_SOURCE_FIELD_SPE
|
|
|
48
48
|
// A real load failure always wins over the host's static/decorative banner
|
|
49
49
|
// text — showing both would just be two banners saying overlapping things.
|
|
50
50
|
const banner = list.error ?? connectionError;
|
|
51
|
-
|
|
51
|
+
const draftTestResultProps = list.testResults[DRAFT_TEST_SCOPE] ? { testResult: list.testResults[DRAFT_TEST_SCOPE] } : {};
|
|
52
|
+
return (_jsxs("section", { className: "external-mcp-tab", children: [_jsxs("div", { className: "external-mcp-head", children: [_jsxs("div", { children: [_jsx("h3", { children: t('External MCP servers') }), _jsx("p", { className: "external-mcp-subtitle", children: t('Third-party tools for your coding agent.') })] }), _jsxs("button", { type: "button", className: "external-mcp-add-button", onClick: () => setFormOpen((open) => !open), "aria-expanded": formOpen, children: [_jsx(Icon, { name: "plus", size: 14 }), _jsx("span", { children: t('Add server') })] })] }), banner ? (_jsx("div", { className: "external-mcp-banner", role: "alert", children: t(banner) })) : null, formOpen ? (_jsx(SourceConfigAddForm, { fieldSpecs: fieldSpecs, values: addForm.values, validation: addForm.validation, submitAttempted: addForm.submitAttempted, submitting: addForm.submitting, submitError: addForm.submitError, onFieldChange: addForm.setField, onTrustChange: addForm.setTrust, onSubmit: () => void addForm.submit(), canTest: list.capabilities.canTest, testing: list.isPending(DRAFT_TEST_SCOPE, 'test'), ...draftTestResultProps, onTest: () => void list.test(undefined, addForm.values), addLabel: "Add server" })) : null, list.loading ? (_jsx("div", { className: "source-config-list-loading", role: "status", children: t('Loading…') })) : (_jsx(ExternalMcpSources, { list: list, fieldSpecs: fieldSpecs, trustOptions: trustOptions })), _jsx(ExternalMcpFooter, { saveStatusLabel: saveStatusLabel, configPath: configPath })] }));
|
|
53
|
+
}
|
|
54
|
+
/** The configured servers, or the empty-state hint when there are none yet. */
|
|
55
|
+
function ExternalMcpSources({ list, fieldSpecs, trustOptions, }) {
|
|
56
|
+
const t = useT();
|
|
57
|
+
if (list.sources.length === 0) {
|
|
58
|
+
return (_jsxs("div", { className: "external-mcp-empty", children: [_jsx("p", { className: "external-mcp-empty-title", children: t('No MCP servers configured.') }), _jsx("p", { className: "external-mcp-empty-hint", children: t('Click "Add server" to get started — pick a template or set up a custom stdio / HTTP server.') })] }));
|
|
59
|
+
}
|
|
60
|
+
return (_jsx("div", { className: "source-config-list-items", children: list.sources.map((source) => (_jsx(SourceConfigItemCard, { source: source, fieldSpecs: fieldSpecs, ...(trustOptions ? { trustOptions } : {}), capabilities: list.capabilities, removing: list.isPending(source.id, 'remove'), refreshing: list.isPending(source.id, 'refresh'), settingTrust: list.isPending(source.id, 'trust'), testing: list.isPending(source.id, 'test'), updating: list.isPending(source.id, 'update'), onRefresh: () => void list.refresh(source.id), onRemove: () => void list.remove(source.id), onTrustChange: (trust) => void list.setTrust(source.id, trust), onTest: () => void list.test(source.id), onUpdate: (patch) => void list.update(source.id, patch), ...(list.testResults[source.id] ? { testResult: list.testResults[source.id] } : {}) }, source.id))) }));
|
|
52
61
|
}
|
|
53
62
|
/** Renders the save-status footer as one unit; a path alone does not show it. */
|
|
54
63
|
function ExternalMcpFooter({ saveStatusLabel, configPath }) {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* DOM-shaped value (`File`, event targets) arrives as a parameter.
|
|
4
4
|
*/
|
|
5
5
|
import { FILE_DROPZONE_KIND_BY_EXTENSION } from './constants.js';
|
|
6
|
+
import { trimChars } from '../../utils/trim-chars.js';
|
|
6
7
|
/** Lowercased extension (no dot), or `''` for an extensionless name. */
|
|
7
8
|
export function fileDropzoneExtension(file) {
|
|
8
9
|
return (/\.([a-z0-9]+)$/i.exec(file.name)?.[1] ?? '').toLowerCase();
|
|
@@ -29,7 +30,7 @@ export function fileDropzoneKind(file) {
|
|
|
29
30
|
}
|
|
30
31
|
/** Kinds renderable straight from an object URL (`blob:`). Fonts also need one (`FontFace` loads from a URL); text is read via `FileReader`/`Blob.text()` instead. */
|
|
31
32
|
export function fileDropzoneNeedsObjectUrl(kind) {
|
|
32
|
-
return
|
|
33
|
+
return ['image', 'video', 'audio', 'pdf', 'html', 'font'].includes(kind);
|
|
33
34
|
}
|
|
34
35
|
/** Uppercased, 4-char-capped extension for the glyph badge (e.g. `"a/b.jpeg"` -> `"JPEG"`, extensionless -> `"FILE"`). */
|
|
35
36
|
export function fileDropzoneExtensionLabel(file) {
|
|
@@ -50,7 +51,7 @@ export function fileDropzoneStagingKey(file) {
|
|
|
50
51
|
}
|
|
51
52
|
/** CSS-safe, collision-resistant `@font-face` family name for a staged font at `index`. */
|
|
52
53
|
export function fileDropzoneFontFamilyName(file, index) {
|
|
53
|
-
const slug = file.name.replace(/[^a-z0-9]+/gi, '-')
|
|
54
|
+
const slug = trimChars(file.name.replace(/[^a-z0-9]+/gi, '-'), '-').slice(0, 24);
|
|
54
55
|
return `jini-file-dropzone-font-${index}-${slug || 'font'}`;
|
|
55
56
|
}
|
|
56
57
|
/** Whether a selection is large enough (by count or total bytes) to warrant a loading affordance while it's staged. */
|
|
@@ -227,9 +227,8 @@ export function applyCanvasEmbedPlaceholders(body, describe) {
|
|
|
227
227
|
if (existingCard && !canRefreshRuntimeCard({ el, card: existingCard }, {}))
|
|
228
228
|
continue;
|
|
229
229
|
const descriptor = describe(el);
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
updatePlaceholder({ el, doc, descriptor, existingCard }, {});
|
|
230
|
+
if (descriptor)
|
|
231
|
+
updatePlaceholder({ el, doc, descriptor, existingCard }, {});
|
|
233
232
|
}
|
|
234
233
|
}
|
|
235
234
|
function canRefreshRuntimeCard({ el, card }, _options = {}) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { trimTrailingChars } from '../../utils/trim-chars.js';
|
|
1
2
|
/**
|
|
2
3
|
* `editor.getCss()` always returns single-line, semicolon-packed CSS — confirmed directly in
|
|
3
4
|
* GrapesJS's own bundled source (`CssRule.prototype.toCSS`/`getDeclaration`): there is no
|
|
@@ -65,7 +66,7 @@ export function prettifyCss(css) {
|
|
|
65
66
|
return false;
|
|
66
67
|
};
|
|
67
68
|
const openRule = (_input, _options = {}) => {
|
|
68
|
-
out = out
|
|
69
|
+
out = trimTrailingChars(out, ' \t');
|
|
69
70
|
out += ' {';
|
|
70
71
|
depth++;
|
|
71
72
|
atDeclStart = true;
|
|
@@ -77,7 +78,7 @@ export function prettifyCss(css) {
|
|
|
77
78
|
};
|
|
78
79
|
const closeRule = (_input, _options = {}) => {
|
|
79
80
|
depth = Math.max(0, depth - 1);
|
|
80
|
-
out = out
|
|
81
|
+
out = trimTrailingChars(out, ' \t');
|
|
81
82
|
// A `}` immediately after `{` (an empty rule) needs no newline before it; otherwise close on
|
|
82
83
|
// its own indented line.
|
|
83
84
|
if (!out.endsWith('{'))
|
|
@@ -137,10 +138,11 @@ export function prettifyCss(css) {
|
|
|
137
138
|
out += ' ';
|
|
138
139
|
i++;
|
|
139
140
|
skipWhitespace({}, {});
|
|
140
|
-
continue;
|
|
141
141
|
}
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
else {
|
|
143
|
+
out += ch;
|
|
144
|
+
i++;
|
|
145
|
+
}
|
|
144
146
|
}
|
|
145
147
|
return out.trim();
|
|
146
148
|
}
|
|
@@ -34,11 +34,54 @@ const RUNTIME_EFFECTS = new Set([
|
|
|
34
34
|
const TEXT_EFFECT_TAGS = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'li', 'dt', 'dd', 'blockquote', 'figcaption', 'label']);
|
|
35
35
|
const PHRASING_TAGS = new Set([...INLINE_TAGS, 'abbr', 'br', 'cite', 'code', 'data', 'dfn', 'img', 'kbd', 'q', 'samp', 'time', 'var', 'wbr']);
|
|
36
36
|
const isPhrasingOnly = (el) => el.children.every(child => PHRASING_TAGS.has(child.tagName) && isPhrasingOnly(child));
|
|
37
|
+
/** Closing quotes for each suffix, honoring the same backslash escapes as effect string literals.
|
|
38
|
+
* A failed quoted token can contain another valid token; suffix lookup preserves that recovery
|
|
39
|
+
* without repeatedly scanning an unterminated string. O(n) time and temporary memory. */
|
|
40
|
+
function effectQuoteEnds({ text, quote }, _options = {}) {
|
|
41
|
+
const ends = new Int32Array(text.length + 2).fill(-1);
|
|
42
|
+
for (let i = text.length - 1; i >= 0; i--) {
|
|
43
|
+
const char = text[i];
|
|
44
|
+
if (char === quote)
|
|
45
|
+
ends[i] = i;
|
|
46
|
+
else if (char === '\\' && /[\r\n\u2028\u2029]/.test(text[i + 1] ?? ''))
|
|
47
|
+
ends[i] = -1;
|
|
48
|
+
else
|
|
49
|
+
ends[i] = ends[i + (char === '\\' ? 2 : 1)];
|
|
50
|
+
}
|
|
51
|
+
return ends;
|
|
52
|
+
}
|
|
53
|
+
/** Remove complete quoted strings and parenthesized arguments before splitting effect names.
|
|
54
|
+
* The first closing parenthesis wins, as in the existing effect grammar. O(n) time and memory. */
|
|
55
|
+
function withoutEffectArguments({ text }, _options = {}) {
|
|
56
|
+
const double = effectQuoteEnds({ text, quote: '"' });
|
|
57
|
+
const single = effectQuoteEnds({ text, quote: "'" });
|
|
58
|
+
const lastClose = text.lastIndexOf(')');
|
|
59
|
+
const parts = [];
|
|
60
|
+
let start = 0;
|
|
61
|
+
let i = 0;
|
|
62
|
+
while (i < text.length) {
|
|
63
|
+
let end = -1;
|
|
64
|
+
if (text[i] === '"')
|
|
65
|
+
end = double[i + 1];
|
|
66
|
+
if (text[i] === "'")
|
|
67
|
+
end = single[i + 1];
|
|
68
|
+
if (text[i] === '(' && i < lastClose)
|
|
69
|
+
end = text.indexOf(')', i + 1);
|
|
70
|
+
if (end >= 0) {
|
|
71
|
+
parts.push(text.slice(start, i));
|
|
72
|
+
i = end;
|
|
73
|
+
start = end + 1;
|
|
74
|
+
}
|
|
75
|
+
i++;
|
|
76
|
+
}
|
|
77
|
+
parts.push(text.slice(start));
|
|
78
|
+
return parts.join('');
|
|
79
|
+
}
|
|
37
80
|
const isEditableEffect = (el) => {
|
|
38
81
|
if (el.attributes['data-editor'] === 'block')
|
|
39
82
|
return false;
|
|
40
83
|
// Quoted selectors and timing/color functions may contain commas, which are not effect separators.
|
|
41
|
-
const effects = (el.attributes['data-kui'] ?? ''
|
|
84
|
+
const effects = withoutEffectArguments({ text: el.attributes['data-kui'] ?? '' })
|
|
42
85
|
.split(',').map(segment => segment.trim().split(/\s+/)[0]).filter(Boolean);
|
|
43
86
|
if (effects.some(effect => RUNTIME_EFFECTS.has(effect)))
|
|
44
87
|
return false;
|
|
@@ -72,10 +115,16 @@ function describeBlock({ el, isLive, hasLiveDescendants }, options = {}) {
|
|
|
72
115
|
reason = 'embed';
|
|
73
116
|
return { reason, host };
|
|
74
117
|
}
|
|
118
|
+
/** `.first.second` from an element's class attribute (first two classes), or undefined when it has none. */
|
|
119
|
+
function classIdentity(className) {
|
|
120
|
+
if (!className)
|
|
121
|
+
return undefined;
|
|
122
|
+
return `.${className.trim().split(/\s+/).slice(0, 2).join('.')}`;
|
|
123
|
+
}
|
|
75
124
|
function createEditorBlock({ el, reason, host }, options = {}) {
|
|
76
125
|
const t = options.translate ?? ((key) => key);
|
|
77
126
|
const id = el.attributes.id;
|
|
78
|
-
const identity = id ? `#${id}` : el.attributes.class
|
|
127
|
+
const identity = id ? `#${id}` : classIdentity(el.attributes.class) ?? el.tagName;
|
|
79
128
|
return {
|
|
80
129
|
...el.outerRange, tagName: el.tagName, ...(id ? { id } : {}), reason, element: el,
|
|
81
130
|
descriptor: reason === 'embed' && host ? host : {
|
|
@@ -155,8 +204,10 @@ export function prepareEditorSource({ html }, options = {}) {
|
|
|
155
204
|
instances.sort((a, b) => a.outerStart - b.outerStart);
|
|
156
205
|
let markerAttribute = 'data-jini-raw-source-0';
|
|
157
206
|
let namespace = 0;
|
|
158
|
-
while (html.toLowerCase().includes(markerAttribute))
|
|
159
|
-
|
|
207
|
+
while (html.toLowerCase().includes(markerAttribute)) {
|
|
208
|
+
namespace += 1;
|
|
209
|
+
markerAttribute = `data-jini-raw-source-${namespace}`;
|
|
210
|
+
}
|
|
160
211
|
const prepared = renderSourceShells({ html, instances, blocks: detected.blocks, markerAttribute }, {});
|
|
161
212
|
return { ok: true, html: prepared, markerAttribute, instances, canvasCss: css.join('\n') };
|
|
162
213
|
}
|
|
@@ -175,7 +226,8 @@ function renderSourceShells({ html, instances, blocks, markerAttribute }, _optio
|
|
|
175
226
|
.map(name => ` ${name}="${escapeAttribute(attrs[name])}"`).join('');
|
|
176
227
|
const tag = instance.nonvisual ? 'span' : instance.tagName;
|
|
177
228
|
const voidTag = instance.openingEnd === instance.outerEnd;
|
|
178
|
-
const
|
|
229
|
+
const closingTag = voidTag && !instance.nonvisual ? '' : `</${tag}>`;
|
|
230
|
+
const shell = `<${tag} ${markerAttribute}="${i}"${layout}>${closingTag}`;
|
|
179
231
|
prepared = prepared.slice(0, instance.outerStart) + shell + prepared.slice(instance.outerEnd);
|
|
180
232
|
}
|
|
181
233
|
return prepared;
|
|
@@ -186,7 +238,9 @@ export function findEditorBlockOpening({ html, target }, _options = {}) {
|
|
|
186
238
|
let result;
|
|
187
239
|
const walk = (el) => {
|
|
188
240
|
if (el.tagName === target.tagName.toLowerCase() && (target.id === undefined || el.attributes.id === target.id)) {
|
|
189
|
-
|
|
241
|
+
const index = occurrence;
|
|
242
|
+
occurrence += 1;
|
|
243
|
+
if (index === target.occurrenceIndex && el.outerRange) {
|
|
190
244
|
const { outerStart: start, openingEnd: end } = el.outerRange;
|
|
191
245
|
result = { start, end, openingTag: html.slice(start, end) };
|
|
192
246
|
}
|
|
@@ -77,10 +77,12 @@ function selectedSourceStart({ wrapper, selected, original, html }, _options = {
|
|
|
77
77
|
let copiesBefore = 0;
|
|
78
78
|
let reached = false;
|
|
79
79
|
visit(wrapper, component => {
|
|
80
|
-
if (component === selected)
|
|
80
|
+
if (component === selected) {
|
|
81
81
|
reached = true;
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (!reached && component.is(PROTECTED_ELEMENT_TYPE) && protectedElementHtml({ component }, {}) === original)
|
|
85
|
+
copiesBefore += 1;
|
|
84
86
|
});
|
|
85
87
|
let start = -1;
|
|
86
88
|
for (let i = 0; i <= copiesBefore; i++)
|
|
@@ -227,7 +227,7 @@ export function useInteractiveHtmlEditor(html, onChange, isProtectedElement, can
|
|
|
227
227
|
useEffect(() => {
|
|
228
228
|
const wrapper = wrapperRef.current;
|
|
229
229
|
if (!wrapper)
|
|
230
|
-
return;
|
|
230
|
+
return undefined;
|
|
231
231
|
const sourcePlan = prepareEditorSource({ html }, { isProtectedElement, describeHostElement: describeEmbedPlaceholder, translate: options.translate });
|
|
232
232
|
let integrityFailure;
|
|
233
233
|
const reportSourceError = (error) => {
|
|
@@ -237,7 +237,7 @@ export function useInteractiveHtmlEditor(html, onChange, isProtectedElement, can
|
|
|
237
237
|
};
|
|
238
238
|
if (!sourcePlan.ok) {
|
|
239
239
|
reportSourceError(new EditorSourceError({ message: sourcePlan.problem }, {}));
|
|
240
|
-
return;
|
|
240
|
+
return undefined;
|
|
241
241
|
}
|
|
242
242
|
const mountEl = document.createElement('div');
|
|
243
243
|
mountEl.style.height = '100%';
|
|
@@ -249,7 +249,7 @@ export function useInteractiveHtmlEditor(html, onChange, isProtectedElement, can
|
|
|
249
249
|
catch {
|
|
250
250
|
reportSourceError(new EditorSourceError());
|
|
251
251
|
mountEl.remove();
|
|
252
|
-
return;
|
|
252
|
+
return undefined;
|
|
253
253
|
}
|
|
254
254
|
const decorate = () => {
|
|
255
255
|
protectTextAncestors({ editor }, {});
|
|
@@ -34,9 +34,7 @@ function recordInnerRange({ node, currentPath, out }, _options = {}) {
|
|
|
34
34
|
function indexChildren(children, parentPath, out) {
|
|
35
35
|
let elementIndex = 0;
|
|
36
36
|
for (const node of children) {
|
|
37
|
-
if (!isElement(node))
|
|
38
|
-
continue;
|
|
39
|
-
if (UNINDEXED_TAGS.has(node.tagName))
|
|
37
|
+
if (!isElement(node) || UNINDEXED_TAGS.has(node.tagName))
|
|
40
38
|
continue;
|
|
41
39
|
const currentPath = [...parentPath, elementIndex];
|
|
42
40
|
recordInnerRange({ node, currentPath, out }, {});
|
|
@@ -134,7 +132,11 @@ export function indexSourceElements({ html }, _options = {}) {
|
|
|
134
132
|
let slot = 0;
|
|
135
133
|
const elements = nodes.filter(isElement).map(node => {
|
|
136
134
|
const atom = UNINDEXED_TAGS.has(node.tagName);
|
|
137
|
-
|
|
135
|
+
let path = null;
|
|
136
|
+
if (!atom) {
|
|
137
|
+
path = [...parentPath, slot];
|
|
138
|
+
slot += 1;
|
|
139
|
+
}
|
|
138
140
|
const loc = node.sourceCodeLocation;
|
|
139
141
|
const children = walk({ nodes: node.childNodes, parentPath: path ?? parentPath, parentStart: loc?.startTag?.endOffset ?? parentStart, parentEnd: loc?.endTag?.startOffset ?? parentEnd }, {});
|
|
140
142
|
const outerRange = sourceOuterRange({ node, html, parentStart, parentEnd }, {});
|
|
@@ -35,15 +35,15 @@ export function IframeKeepAliveProvider({ children, maxMounted = DEFAULT_MAX_MOU
|
|
|
35
35
|
const pool = useMemo(() => ({
|
|
36
36
|
attach(key, host, create) {
|
|
37
37
|
let entry = entriesRef.current.get(key);
|
|
38
|
-
if (!
|
|
39
|
-
entry = { key, element: create(), lastUsedAt: Date.now() };
|
|
40
|
-
entriesRef.current.set(key, entry);
|
|
41
|
-
}
|
|
42
|
-
else if (!activeKeysRef.current.has(key)) {
|
|
38
|
+
if (entry && !activeKeysRef.current.has(key)) {
|
|
43
39
|
// Reattaching a parked entry — undo the hidden/inert markers
|
|
44
40
|
// `release()` set, or the reused iframe would stay invisible.
|
|
45
41
|
unparkIframeElement(entry.element);
|
|
46
42
|
}
|
|
43
|
+
if (!entry) {
|
|
44
|
+
entry = { key, element: create(), lastUsedAt: Date.now() };
|
|
45
|
+
entriesRef.current.set(key, entry);
|
|
46
|
+
}
|
|
47
47
|
entry.lastUsedAt = Date.now();
|
|
48
48
|
activeKeysRef.current.add(key);
|
|
49
49
|
host.appendChild(entry.element);
|
|
@@ -96,7 +96,9 @@ function DeeplinkInstallRow({ deeplink, info, agentHandle }) {
|
|
|
96
96
|
/** The generated snippet, or a loading / could-not-resolve placeholder while there is none. */
|
|
97
97
|
function InstallSnippet({ resolved, loading, error }) {
|
|
98
98
|
const t = useT();
|
|
99
|
-
|
|
99
|
+
const errorPlaceholder = error ? t('Could not resolve install paths.') : '';
|
|
100
|
+
const placeholder = loading ? t('Loading install paths…') : errorPlaceholder;
|
|
101
|
+
return (_jsx(SnippetBlock, { snippet: resolved?.snippet ?? '', language: resolved?.language ?? 'json', placeholder: placeholder }));
|
|
100
102
|
}
|
|
101
103
|
/** Build/Node warning, shown only once install info has loaded and reports a missing prerequisite. */
|
|
102
104
|
function PrerequisiteWarning({ info }) {
|