From 75dccaa320eea2afc2228d44e292e4dfc573feb7 Mon Sep 17 00:00:00 2001 From: 2233admin <2233admin@users.noreply.github.com> Date: Mon, 27 Jul 2026 23:39:42 +0800 Subject: [PATCH 1/4] feat(studio): reorganize OpenCLI preset catalog --- frontend/components/flow/command-palette.tsx | 217 ++++++++++++++---- ...ck-node-capability-catalog-regressions.mjs | 10 + 2 files changed, 180 insertions(+), 47 deletions(-) diff --git a/frontend/components/flow/command-palette.tsx b/frontend/components/flow/command-palette.tsx index 8f3b2a0..820310f 100644 --- a/frontend/components/flow/command-palette.tsx +++ b/frontend/components/flow/command-palette.tsx @@ -154,6 +154,9 @@ const PALETTE_COPY = { opencliCapabilities: "OpenCLI 能力预设", loadingOpencli: "正在读取 OpenCLI 能力目录", featuredSites: "国内全网 OODA 数据源", + catalogIndex: "能力导航", + featuredDescription: "按 OODA 使用场景浏览常用国内数据源", + moreOpencliPresets: "更多站点能力", pluginTools: "插件与后端工具", noTools: "没有匹配的工具", createImport: "创建与导入", @@ -215,6 +218,9 @@ const PALETTE_COPY = { opencliCapabilities: "OpenCLI capability presets", loadingOpencli: "Loading the OpenCLI capability catalog", featuredSites: "China-wide OODA sources", + catalogIndex: "Capability navigation", + featuredDescription: "Browse common China data sources by OODA use case", + moreOpencliPresets: "More site capabilities", pluginTools: "Plugin & backend tools", noTools: "No matching tools", createImport: "Create & import", @@ -325,6 +331,56 @@ function openCLIPresetUnavailable(item: WorkflowOpenCLIAdapterNode): boolean { return materialization === "unavailable" } +function OpenCLIPickerRow({ + item, + language, + onClick, +}: { + item: WorkflowOpenCLIAdapterNode + language: WorkflowLanguage + onClick: () => void +}) { + const Icon = openCLIPresetKind(item) === "source_slot" ? Globe : Wrench + const presentation = openCLIAdapterNodePresentation(item, language) + const unavailable = openCLIPresetUnavailable(item) + const needsSetup = item.strategy === "cookie" || + openCLIAdapterNodeMaterialization(item) !== "source_slot_ready" + + return ( + + ) +} + export type CompatibleConnectionPort = { handleType: "source" | "target" type: string @@ -704,7 +760,13 @@ export function CommandPalette({ return (
{ if (event.key === "Escape") close() }} role="dialog" aria-modal="true" aria-label={copy.search}> -
event.stopPropagation()}> +
event.stopPropagation()} + > {aiMode ? (
@@ -825,56 +887,117 @@ export function CommandPalette({ <> {toolFilter !== "plugin" ? (
- {copy.opencliCapabilities} {catalogBusy ?
{copy.loadingOpencli}
: null} - {!catalogBusy && commonOpenCLINodes.length > 0 ? ( -
- {copy.featuredSites} - {commonOpenCLIGroups.map((group) => ( -
- {group.label} - {group.nodes.map((item) => { - const presentation = openCLIAdapterNodePresentation(item, language) - const needsSetup = item.strategy === "cookie" || openCLIAdapterNodeMaterialization(item) !== "source_slot_ready" - return ( - addOpenCLIAdapter(item)} - trailing={{openCLIStatusLabel(item, language)}} - /> - ) - })} + {!catalogBusy && matchingOpenCLINodes.length > 0 ? ( +
+ + +
+ {commonOpenCLINodes.length > 0 ? ( +
+
+ + {copy.featuredSites} + {copy.featuredDescription} + + {commonOpenCLINodes.length} +
+
+ {commonOpenCLIGroups.map((group) => ( +
+ {group.label} +
+ {group.nodes.map((item) => ( + addOpenCLIAdapter(item)} + /> + ))} +
+
+ ))} +
+
+ ) : null} + + {opencliPresetGroups.length ? ( +
+
+ {copy.moreOpencliPresets} + {visibleOpenCLINodes.length} +
+
+ {opencliPresetGroups.map(([groupKey, items]) => { + const [site, presetKind] = groupKey.split(":") + const groupLabel = `${site} · ${presetKind === "source_slot" ? copy.dataRead : copy.operationTool}` + return ( +
+ {groupLabel} +
+ {items.map((item) => ( + addOpenCLIAdapter(item)} + /> + ))} +
+
+ ) + })} +
+
+ ) : null} +
) : null} - {!catalogBusy ? opencliPresetGroups.map(([groupKey, items]) => { - const [site, presetKind] = groupKey.split(":") - const groupLabel = `${site} · ${presetKind === "source_slot" ? copy.dataRead : copy.operationTool}` - return ( -
- {groupLabel} - {items.map((item) => { - const presentation = openCLIAdapterNodePresentation(item, language) - const unavailable = openCLIPresetUnavailable(item) - return ( - addOpenCLIAdapter(item)} - trailing={{openCLIStatusLabel(item, language)}} - /> - ) - })} -
- ) - }) : null} {!catalogBusy && matchingOpenCLINodes.length > commonOpenCLINodes.length + visibleOpenCLINodes.length ? (

{copy.shown} {commonOpenCLINodes.length + visibleOpenCLINodes.length} / {matchingOpenCLINodes.length}. {copy.refineSearch} diff --git a/frontend/scripts/check-node-capability-catalog-regressions.mjs b/frontend/scripts/check-node-capability-catalog-regressions.mjs index 1681638..369a97d 100644 --- a/frontend/scripts/check-node-capability-catalog-regressions.mjs +++ b/frontend/scripts/check-node-capability-catalog-regressions.mjs @@ -322,6 +322,16 @@ test('tool picker exposes access and readiness as separate filter groups', async assert.match(palette, /role="group" aria-label=\{copy\.readinessFilter\}/) }) +test('OpenCLI preset results use a navigable two-pane catalog without changing node creation', async () => { + const palette = await readFrontendSource('components/flow/command-palette.tsx') + + assert.match(palette, /data-testid="opencli-preset-layout"/) + assert.match(palette, /data-testid="opencli-group-navigation"/) + assert.match(palette, /href=\{`#opencli-group-\$\{group\.id\}`\}/) + assert.match(palette, /lg:grid-cols-2/) + assert.match(palette, /onClick=\{\(\) => addOpenCLIAdapter\(item\)\}/) +}) + test('Studio materializes every searchable OpenCLI capability preset as a node', async () => { const [adapterNodes, adapterCatalog, palette, editor] = await Promise.all([ importTypeScript('lib/workflow/backend-opencli-adapter-nodes.ts'), From 823f667e4e25f2468a801c114613c76b36d31035 Mon Sep 17 00:00:00 2001 From: 2233admin <2233admin@users.noreply.github.com> Date: Mon, 27 Jul 2026 23:58:05 +0800 Subject: [PATCH 2/4] fix(studio): keep inspector configuration scrollable --- frontend/app/(app)/studio/workflow/page.tsx | 2 +- .../components/flow/workflow-canvas-surface.tsx | 4 ++-- frontend/scripts/check-workflow-regressions.mjs | 16 ++++++++++++++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/frontend/app/(app)/studio/workflow/page.tsx b/frontend/app/(app)/studio/workflow/page.tsx index 942e7e5..0b82f50 100644 --- a/frontend/app/(app)/studio/workflow/page.tsx +++ b/frontend/app/(app)/studio/workflow/page.tsx @@ -5,7 +5,7 @@ import { WorkflowProjectHeader } from '@/components/studio/workflow-project-head export default function WorkspaceWorkflowPage() { return ( -

+
}> diff --git a/frontend/components/flow/workflow-canvas-surface.tsx b/frontend/components/flow/workflow-canvas-surface.tsx index 0396375..6038f23 100644 --- a/frontend/components/flow/workflow-canvas-surface.tsx +++ b/frontend/components/flow/workflow-canvas-surface.tsx @@ -273,7 +273,7 @@ export function WorkflowCanvasSurface(props: WorkflowCanvasSurfaceProps) { ) const inspectorVisible = props.inspectorOpen && !props.projectSettingsOpen && !props.settingsOpen return ( -
+
-
+
diff --git a/frontend/scripts/check-workflow-regressions.mjs b/frontend/scripts/check-workflow-regressions.mjs index ed6cd3a..6bbc245 100644 --- a/frontend/scripts/check-workflow-regressions.mjs +++ b/frontend/scripts/check-workflow-regressions.mjs @@ -883,8 +883,20 @@ test('the right workflow dock keeps selection, locates nodes, and allows empty P assert.match(shell, /aria-label="定位到当前节点"/) assert.match(shell, /onPointerDown=\{\(event\) => event\.stopPropagation\(\)\}/) assert.match(surface, //) - assert.match(surface, /className=\{cn\(!inspectorVisible && ["']hidden["']\)\}/) - assert.match(surface, /className="flex min-w-0 flex-1 overflow-hidden"/) +}) + +test('the inspector host constrains long node configuration so the dock owns vertical scrolling', async () => { + const [page, surface] = await Promise.all([ + readSource('app/(app)/studio/workflow/page.tsx'), + readSource('components/flow/workflow-canvas-surface.tsx'), + ]) + + assert.match(page, /className="flex h-\[calc\(100dvh-3\.5rem\)\] min-h-0 min-w-0 flex-col overflow-hidden"/) + assert.match(surface, /className="flex min-h-0 min-w-0 flex-1 overflow-hidden"/) + assert.match( + surface, + /className=\{cn\("h-full min-h-0 overflow-hidden", !inspectorVisible && ["']hidden["']\)\}/, + ) }) test('Houdini-style wiring uses native lifecycle hooks without validation toast side effects', async () => { From 838c7a084c7fd64eb9a1df9be607ff46c2b7c31f Mon Sep 17 00:00:00 2001 From: 2233admin <2233admin@users.noreply.github.com> Date: Tue, 28 Jul 2026 00:41:36 +0800 Subject: [PATCH 3/4] fix(studio): keep node identity consistent --- frontend/components/flow/inspector.tsx | 60 ++++++++++++++----- .../components/flow/nodes/workflow-node.tsx | 33 ++++++---- frontend/lib/workflow/node-i18n.ts | 7 +++ ...ck-node-capability-catalog-regressions.mjs | 17 +++++- 4 files changed, 91 insertions(+), 26 deletions(-) diff --git a/frontend/components/flow/inspector.tsx b/frontend/components/flow/inspector.tsx index aad65da..486c70f 100644 --- a/frontend/components/flow/inspector.tsx +++ b/frontend/components/flow/inspector.tsx @@ -50,6 +50,7 @@ import { getNodeDisplayId, localizeNodeParameterText, localizeNodeText, + shouldPreserveNodeAuthoredText, type WorkflowLanguage, } from "@/lib/workflow/node-i18n" import { buildWorkflowOutlineRows } from "@/lib/workflow/workflow-outline" @@ -72,6 +73,7 @@ import { import type { WorkflowCapability, WorkflowNodeKind, + WorkflowProject, WorkflowProjectNode, } from "@/lib/workflow/schema" import { MonoRow, PanelShell, SectionCaption } from "./inspector-shell" @@ -347,6 +349,7 @@ function InspectorModeTabs({ } function WorkflowOutlinePanel({ + businessLevel, compact, edges, language, @@ -358,7 +361,9 @@ function WorkflowOutlinePanel({ onSelectNode, selectedNodeId, title, + workflowProject, }: { + businessLevel: boolean compact: boolean edges: WorkflowEdge[] language: WorkflowLanguage @@ -370,6 +375,7 @@ function WorkflowOutlinePanel({ onSelectNode: (nodeId: string) => void selectedNodeId?: string title: string + workflowProject: WorkflowProject }) { const copy = INSPECTOR_COPY[language] const nodeById = new Map(nodes.map((node) => [node.id, node])) @@ -381,11 +387,28 @@ function WorkflowOutlinePanel({ {sectionRows.map((row) => { const node = nodeById.get(row.nodeId) if (!node) return null - const localized = localizeNodeText( + const projectNode = findWorkflowProjectNodeByCanvasId(workflowProject, node.id) + const configurationNode = findImplementationNode(projectNode) ?? projectNode + const nodeViewContract = buildCanonicalNodeViewContract(projectNode, node.data, node.id) + const systemText = localizeNodeText( getNodeDisplayId(node.data), { label: node.data.label, description: node.data.description }, language, ) + const prefersCustomLabel = + projectNode?.ui?.preferCustomLabel === true || shouldPreserveNodeAuthoredText(node.data) + const localized = prefersCustomLabel + ? { label: node.data.label, description: node.data.description } + : systemText + const displayLabel = businessLevel && !prefersCustomLabel + ? businessNodeName({ + label: localized.label, + kind: nodeViewContract.identity.kind as WorkflowNodeKind, + capability: nodeViewContract.identity.capability as WorkflowCapability, + params: configurationNode?.params ?? readCanonical(node.data)?.params, + language, + }) + : localized.label return (
diff --git a/frontend/components/flow/nodes/workflow-node.tsx b/frontend/components/flow/nodes/workflow-node.tsx index 5345c4f..d69ecd6 100644 --- a/frontend/components/flow/nodes/workflow-node.tsx +++ b/frontend/components/flow/nodes/workflow-node.tsx @@ -5,7 +5,12 @@ import { Handle, Position, useStore, useUpdateNodeInternals, type NodeProps } fr import type { WorkflowNode as WorkflowNodeType } from "@/lib/flow/types" import { useFlowStore } from "@/lib/flow/store" import { useSettingsStore } from "@/lib/flow/settings-store" -import { getNodeDisplayId, localizeNodeText, type WorkflowLanguage } from "@/lib/workflow/node-i18n" +import { + getNodeDisplayId, + localizeNodeText, + shouldPreserveNodeAuthoredText, + type WorkflowLanguage, +} from "@/lib/workflow/node-i18n" import { getNodeVisualSignature } from "@/lib/workflow/node-visuals" import { runtimeStatusLabel, runtimeStatusTone } from "@/lib/workflow/capabilities" import { buildCanonicalNodeViewContract } from "@/lib/workflow/canonical-node-contract" @@ -365,17 +370,25 @@ function WorkflowNodeComponent({ id, data, selected }: NodeProps sum + batch.itemCount, 0) ?? 0 diff --git a/frontend/lib/workflow/node-i18n.ts b/frontend/lib/workflow/node-i18n.ts index 60831e0..fe68b11 100644 --- a/frontend/lib/workflow/node-i18n.ts +++ b/frontend/lib/workflow/node-i18n.ts @@ -779,6 +779,13 @@ export function localizeNodeText(id: string | undefined, fallback: LocalizedNode return NODE_TEXT[id]?.[language] ?? fallback } +export function shouldPreserveNodeAuthoredText(data: WorkflowNodeData): boolean { + const id = getNodeDisplayId(data) + if (!id || !NODE_TEXT[id]) return false + const label = data.label.trim() + return label !== NODE_TEXT[id]["zh-CN"].label && label !== NODE_TEXT[id]["en-US"].label +} + export function localizeNodeParameterText( id: string, fallback: LocalizedNodeText, diff --git a/frontend/scripts/check-node-capability-catalog-regressions.mjs b/frontend/scripts/check-node-capability-catalog-regressions.mjs index 369a97d..9813427 100644 --- a/frontend/scripts/check-node-capability-catalog-regressions.mjs +++ b/frontend/scripts/check-node-capability-catalog-regressions.mjs @@ -144,6 +144,19 @@ test('core catalog nodes and common parameters expose Chinese and English copy', }), 'Collect A-share market data', ) + const customNodeData = { + label: 'A 股多源真实采集', + description: '自定义业务说明', + nodeType: 'action', + category: 'agent', + canonical: { + catalogId: 'package.opencli.multi-source-hda', + kind: 'agent', + capability: 'normalize', + params: { template: 'opencli-multi-source' }, + }, + } + assert.equal(i18n.shouldPreserveNodeAuthoredText(customNodeData), true) }) test('backend node catalog overlays matching nodes without hiding runnable workflow capabilities', async () => { @@ -310,7 +323,9 @@ test('node picker and inspector share the workflow language setting', async () = assert.match(inspector, /localizeNodeParameterText\(\s*field\.binding\.fieldId/) assert.match(inspector, /setLanguage\("language", nextLanguage\)/) assert.match(inspector, /language=\{language\}/) - assert.match(node, /language,\s*\}\)/) + assert.match(inspector, /shouldPreserveNodeAuthoredText/) + assert.match(inspector, /businessLevel && !prefersCustomLabel/) + assert.match(node, /businessNodeName\([\s\S]{0,320}language,\s*\}\)/) }) test('tool picker exposes access and readiness as separate filter groups', async () => { From 2fe67a9ea72eb0616f5814414725aedb88dcf77c Mon Sep 17 00:00:00 2001 From: 2233admin <2233admin@users.noreply.github.com> Date: Tue, 28 Jul 2026 00:42:30 +0800 Subject: [PATCH 4/4] fix(studio): overlay the inspector on compact screens --- frontend/components/flow/inspector-shell.tsx | 6 +++--- frontend/components/flow/workflow-editor-effects.ts | 2 +- frontend/scripts/check-workflow-regressions.mjs | 5 ++++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/frontend/components/flow/inspector-shell.tsx b/frontend/components/flow/inspector-shell.tsx index 9819227..a7079bf 100644 --- a/frontend/components/flow/inspector-shell.tsx +++ b/frontend/components/flow/inspector-shell.tsx @@ -109,10 +109,10 @@ export function PanelShell({ data-dock-mode={compact ? "overlay" : "shared"} style={compact ? undefined : { width }} className={cn( - "z-40 flex shrink-0 flex-col overflow-hidden border border-ops-line bg-ops-panel text-zinc-100 duration-150 animate-in fade-in slide-in-from-right-4", + "flex shrink-0 flex-col overflow-hidden border border-ops-line bg-ops-panel text-zinc-100 duration-150 animate-in fade-in slide-in-from-right-4", compact - ? "fixed bottom-3 right-3 top-3 w-[min(380px,calc(100vw-1.5rem))] rounded-md" - : "relative h-full rounded-md", + ? "fixed bottom-3 right-3 top-3 z-50 w-[min(380px,calc(100vw-1.5rem))] rounded-md" + : "relative z-40 h-full rounded-md", )} aria-label="工作流右侧工具架" onPointerDown={(event) => event.stopPropagation()} diff --git a/frontend/components/flow/workflow-editor-effects.ts b/frontend/components/flow/workflow-editor-effects.ts index 4d75d04..e4016ca 100644 --- a/frontend/components/flow/workflow-editor-effects.ts +++ b/frontend/components/flow/workflow-editor-effects.ts @@ -77,7 +77,7 @@ export function useDismissNodeMenu( export function useCompactViewportMedia(setCompactViewport: Dispatch>) { useEffect(() => { - const media = window.matchMedia("(max-width: 640px)") + const media = window.matchMedia("(max-width: 1024px)") const update = () => setCompactViewport(media.matches) update() media.addEventListener("change", update) diff --git a/frontend/scripts/check-workflow-regressions.mjs b/frontend/scripts/check-workflow-regressions.mjs index 6bbc245..a69f53e 100644 --- a/frontend/scripts/check-workflow-regressions.mjs +++ b/frontend/scripts/check-workflow-regressions.mjs @@ -92,11 +92,12 @@ function sourceSection(source, start, end) { } test('right workflow dock derives its outline from graph structure and opens without a selection', async () => { - const [{ buildWorkflowOutlineRows }, shortcuts, inspector, shell] = await Promise.all([ + const [{ buildWorkflowOutlineRows }, shortcuts, inspector, shell, effects] = await Promise.all([ importTypeScript('lib/workflow/workflow-outline.ts'), readSource('components/flow/workflow-keyboard-shortcuts.ts'), readSource('components/flow/inspector.tsx'), readSource('components/flow/inspector-shell.tsx'), + readSource('components/flow/workflow-editor-effects.ts'), ]) const nodes = [ { id: 'start', position: { x: 0, y: 0 }, data: {} }, @@ -122,8 +123,10 @@ test('right workflow dock derives its outline from graph structure and opens wit assert.match(shell, /aria-label=["']工作流右侧工具架["']/) assert.match(shell, />Workflow Dock {