}>
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 (
- {localized.label}
+ {displayLabel}
{row.branchLabel ? `${row.branchLabel} · ` : ""}{node.data.nodeType}
@@ -749,6 +772,7 @@ export function Inspector({ compact = false, onClose }: { compact?: boolean; onC
if (selected.length !== 1 || nodeTab === "outline") {
return (
)
}
@@ -786,20 +811,25 @@ export function Inspector({ compact = false, onClose }: { compact?: boolean; onC
const nodeTemplate = getNodeTemplate(configurationNode)
const nodeViewContract = buildCanonicalNodeViewContract(projectNode, data, node.id)
const isBusinessLevel = networkStackLength === 0
- const localizedNodeText = projectNode?.ui?.preferCustomLabel === true
+ const localizedSystemText = localizeNodeText(
+ getNodeDisplayId(data),
+ { label: data.label, description: data.description },
+ language,
+ )
+ const prefersCustomLabel =
+ projectNode?.ui?.preferCustomLabel === true || shouldPreserveNodeAuthoredText(data)
+ const localizedNodeText = prefersCustomLabel
? { label: data.label, description: data.description }
- : localizeNodeText(
- getNodeDisplayId(data),
- { label: data.label, description: data.description },
+ : localizedSystemText
+ const businessLabel = prefersCustomLabel
+ ? localizedNodeText.label
+ : businessNodeName({
+ label: localizedNodeText.label,
+ kind: nodeViewContract.identity.kind as WorkflowNodeKind,
+ capability: nodeViewContract.identity.capability as WorkflowCapability,
+ params: configurationNode?.params ?? canonical?.params,
language,
- )
- const businessLabel = businessNodeName({
- label: localizedNodeText.label,
- kind: nodeViewContract.identity.kind as WorkflowNodeKind,
- capability: nodeViewContract.identity.capability as WorkflowCapability,
- params: configurationNode?.params ?? canonical?.params,
- language,
- })
+ })
const parameterInterfaceView = buildParameterInterfaceView({
node: configurationNode,
adapter: projectAdapter,
@@ -1255,7 +1285,7 @@ export function Inspector({ compact = false, onClose }: { compact?: boolean; onC
{copy.businessHelp}
- {copy.systemNode}: {localizedNodeText.label}
+ {copy.systemNode}: {localizedSystemText.label}
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)
assert.match(shell, /data-dock-mode=\{compact \? ["']overlay["'] : ["']shared["']\}/)
+ assert.match(shell, /compact[\s\S]{0,120}z-50/)
assert.match(shell, /aria-label=["']调整右侧工具架宽度["']/)
assert.match(inspector, /pinnedNodeId/)
+ assert.match(effects, /matchMedia\(["']\(max-width: 1024px\)["']\)/)
})
test('node workflow lives inside a project shell while the legacy canvas route redirects', async () => {