feat(server): place generated images beside the reference anchor node

Read a "reference anchor node:" agent directive and lay newly generated
image nodes immediately to the right of that node, inheriting its row and
falling back to its size, for planned, follow-up, and incremental flows.
Generalize the directive parser shared with the image-generator target.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-17 09:06:02 +08:00
parent 300393dae1
commit d87cb46a9e
3 changed files with 145 additions and 4 deletions
+43 -4
View File
@@ -696,6 +696,7 @@ func (s *DesignService) AgentChat(ctx context.Context, projectID string, req des
now := s.now()
actionID := newID()
targetNodeID := imageGeneratorTargetNodeID(req.Messages)
referenceAnchorID := referenceAnchorNodeID(req.Messages)
isolatedImageGenerator := isImageGeneratorRequest(mode, targetNodeID)
directImageMode := isDirectImageGenerationMode(mode, isolatedImageGenerator)
enableWebSearch := req.EnableWebSearch || toolConfigEnablesSearch(req.ToolConfig)
@@ -792,7 +793,8 @@ func (s *DesignService) AgentChat(ctx context.Context, projectID string, req des
if targetNodeID != "" {
project.Nodes = markImageGeneratorTargetGenerating(project.Nodes, targetNodeID, prompt, req.ImageSize, plannedImageCount(taskPlan))
} else {
project.Nodes = append(project.Nodes, newGeneratingImageNodesForPlan(prompt, len(project.Nodes), taskPlan, req.ImageSize)...)
placeholders := newGeneratingImageNodesForPlan(prompt, len(project.Nodes), taskPlan, req.ImageSize)
project.Nodes = append(project.Nodes, positionGeneratedNodesBesideReference(project.Nodes, referenceAnchorID, placeholders)...)
}
}
project.Connections = nil
@@ -3114,6 +3116,7 @@ func (s *DesignService) completeFollowupGeneration(ctx context.Context, projectI
var plan design.AgentPlan
var incremental bool
targetNodeID := imageGeneratorTargetNodeID(messages)
referenceAnchorID := referenceAnchorNodeID(messages)
if isolated {
plan, incremental, err = s.planWithIncrementalImages(ctx, projectID, threadID, prompt, targetNodeID, false, request)
} else {
@@ -3139,6 +3142,7 @@ func (s *DesignService) completeFollowupGeneration(ctx context.Context, projectI
}
return s.markAgentGenerationFailed(detachedUserContext(ctx), projectID, threadID, err)
}
plan.GeneratedNodes = positionGeneratedNodesBesideReference(project.Nodes, referenceAnchorID, plan.GeneratedNodes)
} else if err := incompleteIncrementalGenerationError(plan, taskPlan); err != nil {
if isolated {
return s.markIsolatedGenerationFailed(detachedUserContext(ctx), projectID, targetNodeID, err)
@@ -3241,6 +3245,10 @@ func (s *DesignService) planWithIncrementalImages(ctx context.Context, projectID
return fmt.Errorf("image generation returned empty image")
}
node := nodes[0]
positioned := positionGeneratedNodesBesideReference(req.Project.Nodes, referenceAnchorNodeID(req.Messages), append(persistedNodes, node))
if len(positioned) > 0 {
node = positioned[len(positioned)-1]
}
persistedNodes = append(persistedNodes, node)
return s.applyIncrementalGeneratedNode(ctx, projectID, threadID, prompt, targetNodeID, node, result.Index, appendArtifacts)
})
@@ -4710,7 +4718,15 @@ func isImageGeneratorRequest(mode string, targetNodeID string) bool {
}
func imageGeneratorTargetNodeID(messages []design.AgentChatMessage) string {
const targetPrefix = "image generator target node:"
return agentDirectiveNodeID(messages, "image generator target node:")
}
func referenceAnchorNodeID(messages []design.AgentChatMessage) string {
return agentDirectiveNodeID(messages, "reference anchor node:")
}
func agentDirectiveNodeID(messages []design.AgentChatMessage, prefix string) string {
prefix = strings.ToLower(strings.TrimSpace(prefix))
for _, message := range messages {
for _, content := range message.Contents {
if strings.ToLower(strings.TrimSpace(content.Type)) != "text" {
@@ -4718,8 +4734,8 @@ func imageGeneratorTargetNodeID(messages []design.AgentChatMessage) string {
}
for _, line := range strings.Split(content.Text, "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(strings.ToLower(line), targetPrefix) {
return strings.TrimSpace(line[len(targetPrefix):])
if strings.HasPrefix(strings.ToLower(line), prefix) {
return strings.TrimSpace(line[len(prefix):])
}
}
}
@@ -4727,6 +4743,29 @@ func imageGeneratorTargetNodeID(messages []design.AgentChatMessage) string {
return ""
}
func positionGeneratedNodesBesideReference(nodes []design.Node, referenceNodeID string, generated []design.Node) []design.Node {
positioned := append([]design.Node(nil), generated...)
referenceIndex := findNodeIndex(nodes, strings.TrimSpace(referenceNodeID))
if referenceIndex < 0 || len(positioned) == 0 {
return positioned
}
reference := nodes[referenceIndex]
gap := math.Max(48, math.Min(120, reference.Width*0.08))
nextX := reference.X + reference.Width + gap
for index := range positioned {
if positioned[index].Width <= 0 {
positioned[index].Width = reference.Width
}
if positioned[index].Height <= 0 {
positioned[index].Height = reference.Height
}
positioned[index].X = nextX
positioned[index].Y = reference.Y
nextX += positioned[index].Width + gap
}
return positioned
}
func mergeGeneratedNodesIntoTarget(nodes []design.Node, generated []design.Node, targetNodeID string) []design.Node {
if strings.TrimSpace(targetNodeID) == "" || len(generated) == 0 {
return append(nodes, completedImageGeneratorNodes(generated)...)