Skip to content

Flow canvas: decision edge labels print the whole CEL condition and overlap (campaign_enrollment) #12507

Closed

Description

@objectstack-fleet

Source

packages/app-shell/src/views/metadata-admin/previews/FlowCanvas.tsx labels a branch edge with edge.isDefault ? 'else' : cond ? \if ${cond}` : edge.label and draws the string at the edge midpoint. On HotCRM "Enroll Members in Campaign" (campaign_enrollment, Automations pillar, 100 % zoom, 1482 px) the two edges out of "Campaign Open?" carry if has(vars.campaignRecord) && has(vars.campaignRecord.status) && (vars.campaignRecord.status == "planning" || vars.campaignRecord.status == "in_progress") && vars.memberSource != "contacts"(147 characters) and its== "contacts"` twin; the two strings run across each other and across the "Find Eligible Leads" and "Find Eligible Contacts" nodes, and neither can be read.

Target

  1. An edge label is one line of at most 32 characters: the edge's own label when it has one, otherwise if followed by the condition clamped with an ellipsis; else unchanged. The label sits in a pill on the canvas background so it never draws over a node card or another label.
  2. The full condition is in the pill's title and in the edge inspector (FlowEdgeInspector.tsx), which already edits it.
  3. Labels longer than the pill wrap nowhere; the pill's width is capped at the node width (260 px on the artboard automations.html).

File surface

packages/app-shell/src/views/metadata-admin/previews/FlowCanvas.tsx (branch label), flow-canvas-parts.tsx if the label element lives there, tests beside them.

Acceptance

  1. A test gives a decision edge a 150-character condition and asserts the rendered label is at most 32 characters plus the ellipsis with the full condition in title; a second asserts an edge with its own label shows that label unclamped when it fits.
  2. campaign_enrollment at 1280 px before/after, light and dark: both labels legible, no overlap.
  3. Every FlowCanvas.*.test.tsx and flow-problems.*.test.ts green.

Constraints

Evidence

Screenshots from the test pass, kept on the temporary branch claude/studio-test-pass-evidence-20261015 of objectstack-ai/objectui (commit 72115588c, never merged: the maintainer ruled that screenshots do not enter main, and asked for a temporary branch referenced from the card). The branch's docs/design/studio-redesign/findings/2026-10-15/README.md maps every file to its card. Pane captures, 800 × 604 of a 1482 × 1119 viewport, dark unless named light.

flow edge labels
docs/design/studio-redesign/findings/2026-10-15/12507-flow-edge-labels.jpg — flow edge labels.

Blocked-by: #12454

Filed by the epic PM session session_cdb7a717-def7-4e71-9988-f089fbaa18ff from the 2026-10-15 Studio test pass: objectui main d4bc81eab, HotCRM 63db83e2, the vite console on :5180 against objectstack serve --dev on :4001, viewport 1482 × 1119, dark theme with a light-theme check.

Parent: #12446

Body refreshed 2026-10-15T15:21Z by the epic PM session: the Evidence section, first added at 2026-10-15T14:42Z, now names the temporary branch the screenshots live on instead of a pull request to main.

Activity

  1. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 (domain:ui lane queue)
    Session: session_01SJn1GEkrbbBQkhGC26wEDw
    Account: os-sales (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-12507-flow-edge-labels
    Worktree: objectui-issue-12507 (in the cloud session's own container)
    Domain: domain:ui
    Seat: domain:ui#4
    File surface: packages/app-shell/src/views/metadata-admin/previews/FlowCanvas.tsx (the branch-label string and its element only), previews/flow-canvas-parts.tsx (only if the label element lives there), new tests beside them, the screenshots under docs/design/studio-redesign/after/ named 12507-*.png, one .changeset/12507-*.md (stop on breach; explain in the report)
    Container & model: M, mode:cloud, model: opus (cloud on the maintainer's stance 「可以的话,ui 后续开发尽量用云 session,并且要求截图给我看」; lane scope on 「你也可以接ui车道的卡」; batch 5 on 「上限提到 5,提速」, this session's chat, 2026-10-15)
    Clause-②: no
    Responsibility: n/a — canvas label presentation, no edge semantics
    Thread-read: none
    Serial constraints cleared: Blocked-by: #12454 is closed (PR objectui#12481, 8146afe). objectui#12488 (PR objectui#12543, a27c1cf, the node cards in flow-canvas-parts.tsx) landed minutes ago, so the flow canvas region is free. No open PR touches FlowCanvas.tsx. The · N outcomes count in nodeSummary (the #12543 ACCEPT's open question 2) is not in this card's target and is not carried.

  2. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 12507,
    "status": "done",
    "branch": "claude/issue-12507-flow-edge-labels",
    "pr": null,
    "session": "session_01XQJAgrVmJoDoSahLkQ6WMf",
    "premise_still_valid": true,
    "summary": "The card's main premise holds, but one part of it is wrong. Correct: the branch-label string is the one the card quotes, in FlowCanvas.tsx at about :1028 on main 1ffd6a2, and it puts the condition ahead of the edge's own label. So HotCRM campaign_enrollment e4/e7 printed their 147-character CEL condition and never showed 'Open · Leads' / 'Open · Contacts'. Wrong: the strings never ran across each other or across the node cards. On d4bc81e and on main the pill already sat in a 120px foreignObject with CSS truncate. The card's own evidence screenshot shows two pills that do not overlap and both read 'if has(vars.campaign…'. The real defect is that the two pills could not be told apart. Fix: the edge's own label now reads first, otherwise 'if ' plus the condition, and 'else' is unchanged. The text is clamped to 32 code points plus an ellipsis, and the full wording (label and condition, one per line) goes in the pill's title. The pill's box may be as wide as a node card (NODE_W, 240) but stops short of the nearest other pill on the same band. The designer's insert handle now sits just past the drawn pill instead of at a fixed +66. Card assignee was os-sales; it was not written.",
    "tests": "HEAD b516167. New: FlowCanvas.branchLabels-12507.test.tsx, 6 tests: a 150-character condition renders as 32 characters plus the ellipsis with the full condition in title; an own label shows unclamped with its condition in title; an own label over 32 characters is clamped; a default edge reads else with no title; a lone pill is NODE_W wide and sibling pills leave clear space between them; the insert handle clears the pill. Targeted union, run from the repo root: pnpm exec vitest run --maxWorkers=2 over the 20 files, i.e. every FlowCanvas.* test, every flow-problems.* test, flow-canvas-parts.nodeCard-12488, every AutomationsPillar.* test (6), flowFieldChrome.i18n-10862-s4 (it asserts the branch-label text) and the new file. Result: Test Files 20 passed (20), Tests 178 passed (178), lock VERDICT command-exit 0. Ablation: each new pin mutated once through objectstack scripts/ablation-replace.mjs; each anchor hit x1 then x0, and the tests import ./FlowCanvas from source, so no dist was involved. (1) Precedence reversed, ifCond || edge.label: 2 failed. (2) Clamp bound set to 1000: 2 failed. (3) Neighbour limit removed: 1 failed. (4) Handle back at +66: 1 failed. (5) Title dropped: 3 failed. After each run: git diff HEAD empty and FlowCanvas.tsx hash equal to its HEAD blob. The before screenshots were taken with the BASE FlowCanvas.tsx swapped in under a trap that restores it; the restore was confirmed with RESTORED-OK (hash compare).",
    "gates": "All on worktree HEAD d399570 (code) and b516167 (code plus PNGs); exit codes captured before any pipe. pnpm check:i18n-keys: 0. pnpm check:i18n-drift: 0, 'No en value changed in this range'. node scripts/check-changeset-presence.mjs: 0, '2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/12507-flow-edge-labels.md'. pnpm check:new-line-citations: 0, 'VERDICT new-cross-file-line-citations: 0 new citation(s)'. pnpm --filter @object-ui/app-shell type-check: 0; the script echoed type-check, and tsconfig.test.json includes src/**/*.test.tsx, so the new test is type-checked. It needed turbo run build --filter='@object-ui/app-shell^...' --concurrency=2 first (28/28 tasks); the first attempt failed with 1277 TS2307 from unbuilt dependencies, which is NOT MEASURED, not a red. pnpm --filter @object-ui/app-shell build: 0, 'dist completeness: 1 package(s) complete'. eslint on the 2 touched TS files: 0 errors and 1 warning, react-hooks/exhaustive-deps on the existing positionOf useCallback dependency array. That line is not in the diff and the warning is present on main. Union vitest re-run on b516167: 20/20 files, 178/178 tests. No PR exists yet, so CI check-runs are not cited; the PM reads them once the draft PR is open.",
    "mcp_calls": "2 GitHub MCP calls, both reads: mcp__github__issue_read (get) and mcp__github__issue_read (get_comments) on objectui#12507. Also 2 claude-code-remote add_repo calls (objectstack, hotcrm; access: read, both answered read_available, nothing attached). No write tool.",
    "api_writes": "0 REST writes. git pushes to objectstack-ai/objectui: the empty branch at 1ffd6a2, d399570 (fix, test, changeset) and b516167 (screenshots). Not git pushes, but recorded: deleted the stale local branch claude/issue-12507-flow-edge-labels (no commits of its own) from the shared checkout after detaching it, and deleted my temporary ref refs/tmp12507/evidence.",
    "open_questions": [
    {
    "question": "Pill width cap: the card says the pill is capped at the node width (260 on automations.html), and that it never draws over another label. In code NODE_W is 240. Sibling outcomes of one decision have edge midpoints (NODE_W + H_GAP) / 2 = 142px apart, so a 240 or 260 cap would let two long pills overlap. Which rule wins?",
    "options": [
    "A: box = min(NODE_W, distance to the nearest pill on the same band minus 8px). A lone pill gets 240; a two-way fan-out gets 134 each, and a clamped 32-character condition there still CSS-truncates, with the full text in title.",
    "B: a flat 260 or 240 cap; long sibling pills overlap.",
    "C: move pills to the target column above the child node, as on the artboard. That changes anchoring and the insert handle, and two labelled edges into one target would collide."
    ],
    "recommendation": "A, implemented. Real business need: HotCRM's real labels fit in either case, and A keeps the card's no-overlap constraint. Long-term soundness: it is a presentation rule computed from layout constants, with no routing change. Making AI metadata errors harder: pills never hide each other, and the full condition is always in title. Startup focus: it adds no metadata surface and is about 30 lines."
    },
    {
    "question": "Title contents: the card says 'the full condition in title'. For an edge that has both a label and a condition, the title reads 'LABEL' then a newline then 'if CONDITION'. A default edge ('else') gets no title. Keep this?",
    "options": [
    "A: label and condition, one per line (implemented)",
    "B: condition only"
    ],
    "recommendation": "A. A clamped own label must stay recoverable too, and the condition is still the second line."
    }
    ],
    "out_of_scope_findings": [
    "Observation only (not reproduced, so not filed): pills stay at the edge midpoint, which is unchanged. An edge that skips a layer has its midpoint inside the intermediate row, so a labelled skip edge could put its pill over a node card in that row. campaign_enrollment has no such edge (e6/e9 into End carry no label). carrier: none; it goes in the PR's Acceptance notes. Dedupe words: flow canvas skip-layer edge label overlap node, edge midpoint intermediate row, branch pill over node card.",
    "Contract conflict noted: os-dev.md says the dev opens the draft PR and posts the os-dev-report comment. The dispatch's degraded-channel order (fleet-write relay refused with 403 in this container) says not to do either. I followed the dispatch, did neither, and kept pr null. Commit trailers follow objectui AGENTS.md (model-free Claude-Session plus Co-authored-by Claude pair), which wins over the harness attribution reminder."
    ],
    "deviations": [
    "Insert-handle x changed from the fixed labelPos.x + 66 to labelPos.x + branch.handleX. That line is outside 'the branch-label string and its element only', but the +66 was hard-coded to the old 120px pill box; a wider pill would sit under the handle. It is pinned by ablation (4).",
    "The anchor computation inside the edges map (sp/tp, back, sPos/tPos, from, to, labelPos) moved verbatim into an edgeEnds(edge) closure, so a pre-pass can see every pill's anchor before choosing widths. No routing expression changed: edgePath, backEdgePath and the anchors are untouched.",
    "Assumption 3 was falsified on width: NODE_W is 240 in code, not 260, and sibling midpoints are 142px apart. The cap follows open question 1 (A).",
    "Assumption 4: the clamp counts code points with Array.from. There is no Intl call, so no locale threading is needed.",
    "Two new private helpers (branchLabelOf, layoutBranchLabels) and their constants live in FlowCanvas.tsx, not in flow-canvas-layout.ts, which is outside the claimed file surface. Nothing new is exported."
    ],
    "files_changed": [
    "packages/app-shell/src/views/metadata-admin/previews/FlowCanvas.tsx",
    "packages/app-shell/src/views/metadata-admin/previews/FlowCanvas.branchLabels-12507.test.tsx",
    ".changeset/12507-flow-edge-labels.md",
    "docs/design/studio-redesign/after/12507-flow-edge-labels-before-light.png",
    "docs/design/studio-redesign/after/12507-flow-edge-labels-before-dark.png",
    "docs/design/studio-redesign/after/12507-flow-edge-labels-after-light.png",
    "docs/design/studio-redesign/after/12507-flow-edge-labels-after-dark.png"
    ],
    "line_budget": "n/a",
    "pr_title_draft": "fix(app-shell): flow canvas branch pills read the edge's own label, clamped to 32 characters (objectui#12507)",
    "pr_body_draft": "(elided by the seat: opened verbatim as the body of PR #12555, read back identical)"
    }

    Relayed verbatim by seat domain:ui#4 (session session_01SJn1GEkrbbBQkhGC26wEDw) from the cloud session's final message, the degraded write channel. The seat opened the draft PR #12555 from pr_title_draft and pr_body_draft, with assignee os-sales. "pr": null above is the session's own value at report time.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:workflowApprovals and automation — the work that runs without a person driving itbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions