Skip to content

Sequence diagram, highlighting & extension - #248

Draft
rnva wants to merge 303 commits into
mainfrom
summer
Draft

rnva wants to merge 303 commits into
mainfrom
summer

Conversation

@rnva

@rnva rnva commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Getting ready for merging summer into main ... In progress

rnva and others added 30 commits July 7, 2026 13:53
The .then callback in setHandlersForActivityDiagram was not async,
so fetchActivityPositions ran fire-and-forget. The loading overlay
was removed before activityRowMap was populated, meaning
editor-to-diagram hover highlighting silently did nothing for the
brief window until the fetch completed.

Made the .then callback async and added await, matching how the
sequence diagram side awaits all four position fetches before
removing the overlay.
Awaiting fetchActivityPositions (previous commit) increased the
duration of the initial activity render that fires on page load.
test_second_bar_uses_refreshed_message_positions raced against that
render: if it completed after the fixed 5 s timeout, messagePositions
was still empty when the test read it.

Replace the three fixed wait_for_timeout calls in that test with
wait_for_function conditions (messagePositions.length === 3, then
the expected activate Bob count), matching the approach already used
in TestDeleteActivationFlow._load_bar_diagram.
The initial demo render (fired on page load) and the test's sequence
render run concurrently. Their .then callbacks both write to
element.innerHTML, so whichever completes last wins. When the activity
render wins, fetchMessagePositions has already read a stale or empty SVG
and messagePositions never reaches the expected count.

Replace the fragile fixed-timeout _load_diagram with _load_sequence: a
retry loop that keeps calling setValue until messagePositions stabilises
at the expected count, matching the pattern already used in
TestDeleteActivationFlow._load_bar_diagram. Also apply the same approach
to test_second_bar_uses_refreshed_message_positions for its initial load.
Move the editor->diagram hover-highlight machinery (row map, apply/restore,
active-list management) out of activity.js and sequence-operations.js into a
new static/hover-highlight.js. It exposes pure functions over a passed-in row
map and active list (registerHoverRow, highlightHoverRow, clearHoverHighlight,
findActiveHighlight) plus attributeHighlight/stylePropertyHighlight style
factories, so each diagram keeps owning its own state and render walk while the
duplicated machinery lives in one place.

Public globals and function names are unchanged, so the hover e2e suites pass
without modification. Full e2e run: 98 passed.
Hovering an editor row highlights the matching diagram element, but the
sequence diagram-side hover preserves highlights and never resets (unlike
activity's element mouseover), so leaving the editor left the last hovered
element stuck highlighted. Add a mouseleave listener on the Ace container that
clears the active highlight for the current diagram type.

Also consolidate the editor-side hover/cursor dispatch (mousemove, mouseleave,
cursor change) into hover-highlight.js (initEditorHoverHighlighting,
highlightEditorRow, resetEditorHighlight, lastEditorHoverRow), and remove the
now-redundant resetActivityHighlight() from activity's element mouseover so both
diagram types clear the editor-side highlight the same way.
Hovering an activity diagram element highlights its editor line via a hover
marker, but no activity mouseout ever called clearMarkers (unlike the sequence
handlers), so the last hovered element's line stayed highlighted in the editor.
Add a single mouseout listener on the rendered <g> (mouseout bubbles from the
elements; the <g> is recreated each render so it doesn't stack) that clears the
marker when the pointer leaves an element.
The five per-render position fetchers (participant, message, note, group,
activity) all repeated the same request plumbing: grab the rendered SVG, read
the editor puml, POST them as JSON, parse the response, handle failure. Extract
that into a shared fetchDiagramData(endpoint) in hover-highlight.js that returns
the parsed JSON or null; each fetcher keeps only its endpoint and where the
result goes. Standardizes error handling too - a failed fetch now silently
disables hover highlighting for that render, where previously
extractLifelinePositions alone surfaced an error dialog.
Hovering an activity diagram element fetched its editor line from a getXLine
endpoint on every hover (10 processXLine functions). But getActivityPositions
already returns every element's rows once per render. Build the inverse
element->rows map in buildActivityRowMap and mark the editor line via the new
markEditorForElement, so hovering is a synchronous cached lookup with no network
round-trip. Removes the 10 processXLine functions.
With activity diagram-to-editor hover using cached positions, the ten getXLine
routes have no callers. Remove the routes, their tests, the three orphaned
functions (get_note_line/get_group_line/get_arrow_line), and now-unused imports;
update routes.md and the frontend/backend contract. The shared line-finders stay
(still used by positions.py and edit/delete operations).
The four per-render sequence position fetches (participant, message, note,
group) each grabbed the rendered SVG + puml and POSTed them separately, so a
render cost four serialized round-trips with the payload duplicated four times.
Add a single /getSequencePositions endpoint (new sequence/positions.py
aggregator) that bundles all four tables into one response, matching the
activity diagram's one-fetch-per-render pattern. The frontend's four fetchers
become one fetchSequencePositions.

Each sub-table keeps its own geometry-carrying shape (participants carry
lifeline bounds, messages/notes carry SVG Y-coordinates) because sequence
elements are matched spatially, not by ordinal - this is a transport
aggregation, so the data model and the diagram->editor hover logic are
unchanged. Removes the now-dead /getParticipantPositions, /getMessagePositions,
/getSeqNotePositions and /getSeqGroupPositions routes; updates docs and tests.
…ght' into feature/sequence-multiline-note-message-text

# Conflicts:
#	.kiro/steering/structure.md
#	CHANGELOG.md
Address PR review feedback:

- The activity ellipse diagram-side hover set fill to '#818181 ' (trailing
  space). The editor->diagram ELLIPSE_HIGHLIGHT applies the space-free
  '#818181', so its apply guard (old === value) failed to match during a
  simultaneous editor+diagram hover, capturing the spaced value as the
  'original' and restoring it. Removed the trailing space so both directions
  use the identical value.
- The 'Editor <-> Diagram Hover Highlighting (Activity Diagrams)' section in
  the frontend/backend contract still described diagram->editor as using the
  per-element get*Line routes fetched on mouseover. Those routes were removed;
  the direction now uses the cached activityElementRows map via
  markEditorForElement. Updated the section to describe both directions
  resolving client-side from the single per-render getActivityPositions fetch.
feature hover highlight between diagram and ace editor
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…sage-text

Feature/sequence multiline note message text
The activity outside-click handler (addActivityEventListeners) listed
'seq-note-menu' instead of 'note-menu' when closing context menus,
so the activity note context menu never closed on outside click.
The two ids got mixed up between the sequence and activity note menus.
Fix activity note context menu not closing
Show Import button in code toolbar, remove accent from Download
Verified against real PlantUML JAR SVG output that note, hnote, and
rnote render as visually distinct, color-independent shapes: note is
a folded-corner path pair (6-point body + 4-point fold), hnote is a
7-point polygon (hexagon), and rnote is a plain rect. This holds
across all placements (over/left/right/spanning), the message-attached
shortcut, and custom colors.

classify_note_shape() is a pure function, not yet wired into note
detection - that's the next step.
_find_note_line_index and the participant cascade-delete note check
previously only matched lines starting with the literal "note ".
Added note_line_keyword()/is_note_line() to also recognize hnote and
rnote, and to tolerate an optional #color token after the keyword
(e.g. "note #FFAAAA over X : text") for forward compatibility with
a future note-color-editing feature.

This only touches text-side matching. SVG-side detection
(extract_note_positions) still only handles plain notes via fill
color - that's the next step.
extract_note_positions and index_of_clicked_note previously found
notes by filtering SVG paths with fill=#FEFFDD (PlantUML's default
note color). Replaced with iter_note_shapes(), which identifies
note/hnote/rnote candidates by tag + shape signature (from
classify_note_shape) + stroke-width:0.5, excluding look-alikes that
share a tag or fill with notes:

- Participant header rects (also plain <rect>, stroke-width:0.5) are
  excluded via rx/ry, which notes never have.
- Activation bars and group borders/tabs (also collide in tag/point
  count with rnote/note) are excluded via their different
  stroke-width (1.0/1.5 vs notes' 0.5).
- Message arrowheads (4-point <polygon>) are already excluded by
  classify_note_shape's 7-point check for hnote.

index_of_clicked_note now matches the clicked element by tag-
appropriate identity attribute (d/points/x+y+width+height) instead
of only path 'd'. extract_note_positions gained per-tag cy extraction
via _shape_cy.

Also fixed a latent off-by-one in the test helper extract_note_path
(tests/sequence/test_sequence_note.py) that double-counted a note's
fold-corner path as a separate note when locating the Nth note by
index - it happened to cancel out against the old production bug of
the same shape, and was only exposed once production detection was
fixed to be correct.

Verified via live PlantUML JAR that this holds for notes with custom
colors (e.g. white, matching activation bar / group tab colors),
confirming detection no longer depends on the default #FEFFDD fill.
The frontend hover-highlight matching (sequence-operations.js) still
uses #FEFFDD and is unchanged - that is frontend-side follow-up work,
not required for this backend task.
add_note() and _build_note_line() now accept a note_type parameter
(note/hnote/rnote, defaulting to note) that selects the
PlantUML keyword used in all four placement forms and the
message-attached shortcut. All three types share identical placement
grammar, so no placement-specific branching was needed - only the
keyword is substituted.

Added _normalize_note_type() to validate the value server-side
(falls back to "note" for missing/unrecognized input, since it
comes directly from the client). The /addNote route now accepts an
optional noteType field and passes it through.

Backend-only change: the frontend does not send noteType yet, so all
existing UI flows are unaffected and continue to create plain notes.
Updated docs/routes.md and docs/frontend_backend_contract.md to
document the new field.
Clicking "Add Note" now replaces the sequence context menu in place
with a Note / H Note / R Note choice, which itself replaces in place
with the existing placement menu - mirroring the "Add Group" type
submenu pattern already used for group/alt/opt/loop.

Added seq-note-type-menu markup (sequence_menus.html) and wired it in
sequence-operations.js: the choice is stored in selectedNoteType,
reset to "note" by cancelNoteAddMode().

Frontend-only: selectedNoteType is not yet sent to the backend, so
every note created through the UI is still a plain note regardless
of which type is picked. Wiring it into the create request is the
next step.

5 new Playwright tests in tests/e2e/test_sequence_note_menu.py cover
the menu-replaces-itself positioning, per-type storage, and reset on
cancel, following the same page.evaluate() style used for the
existing group type-menu tests.
submitNote() now includes noteType: selectedNoteType in the /addNote
request body, completing the flow from the type submenu (Task 5)
through the backend's note_type parameter (Task 4). H Note and R
Note are now genuinely creatable through the UI end to end, not just
via direct API calls.

selectedNoteType is read before cancelNoteAddMode() resets it (fired
by the modal's hidden.bs.modal handler after modal('hide') and again
by submitNote() itself), so there is no race between building the
request body and the reset.

4 new integration e2e tests in tests/e2e/test_sequence_note_menu.py
drive the full click sequence (type menu -> placement menu -> text
modal -> submit) against the live server and assert on the resulting
PlantUML text for hnote, rnote, and note, plus a reset-after-create
check.

Updated FEATURES.md and .kiro/steering/product.md since this is now
a complete, user-facing feature rather than backend/frontend-only
groundwork.
rnva and others added 21 commits August 31, 2026 23:14
The PNG button rendered document.getText(), which for a Markdown file is a
page of prose with a diagram somewhere in it -- java would have made
nothing useful of it. It now renders the region the panel is showing, the
last of the four places the host reads the document.

A region it cannot find is reported rather than rendered. What is on screen
is not a source this process kept a copy of, the document being the only
authority, so there is nothing to fall back to and a picture of whatever
else is in the file would be worse than a message.

The dialog still opens on <basename>.png beside the document, which for a
file holding several diagrams is the same name for each; naming them apart
would mean inventing a scheme out of line numbers that move.
Bumps the npm_and_yarn group with 1 update in the /plantuml-extension directory: [fast-uri](https://github.com/fastify/fast-uri).


Updates `fast-uri` from 3.1.5 to 3.1.7
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](fastify/fast-uri@v3.1.5...v3.1.7)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.7
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
…l-extension/npm_and_yarn-8839a8980a

chore(deps-dev): bump fast-uri from 3.1.5 to 3.1.7 in /plantuml-extension in the npm_and_yarn group across 1 directory
Change New > Empty default to a placeholder comment
Bumps the npm_and_yarn group with 1 update in the /plantuml-extension directory: [js-yaml](https://github.com/nodeca/js-yaml).


Updates `js-yaml` from 4.3.1 to 4.3.2
- [Changelog](https://github.com/nodeca/js-yaml/blob/4.3.2/CHANGELOG.md)
- [Commits](nodeca/js-yaml@4.3.1...4.3.2)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.3.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
A participant has two names that used to be conflated: the displayed name
PlantUML renders (and therefore the only one readable from the SVG, so the
only one a click can resolve to), and the alias the diagram body refers to.
Nothing modelled the alias at all -- PARTICIPANT_DECLARATION_RE deliberately
discarded it along with the trailing modifiers.

Add ParticipantDeclaration plus parse_participant_declaration and
participant_declarations, splitting a declaration line into keyword, displayed
name, alias and the modifiers after it (order 10, a color, a stereotype), which
are captured verbatim so a rewrite cannot silently drop one. The keyword set now
covers every lifeline keyword rather than `participant` alone; only `participant`
renders the rounded header rect the editor detects, but the others still claim
identifiers that a generated alias must not collide with.

Participant gains `alias`, filled in while attaching declaration lines, and a
`reference_name` property (alias when there is one, else the displayed name).
reference_name_for resolves the same thing from puml alone, for callers that
have no SVG.

No route changes behaviour yet.
Renaming was a single puml.replace of the displayed name, which broke in three
ways: a name with spaces produced invalid PlantUML (`participant Space Room`,
`Alice -> Space Room: hi`), an aliased declaration was rewritten by substring
luck rather than by structure, and every other occurrence of the old name was
rewritten too, including message text and note bodies.

Add sequence/rename.py, which owns renaming as pure puml surgery, and reduce
edit_participant_name to the adapter that resolves the click. Which lines change
depends on the declaration:

  - aliased: only the displayed name, since the body refers to the alias
  - no alias, new name usable as a bare token: declaration and all references
  - no alias, new name not usable as one: the participant gains a generated
    PascalCase alias (`participant "Space Room" as SpaceRoom`) and the
    references switch to it

"Not usable as a bare token" covers whitespace (the motivating case) as well as
#, , and :, which PlantUML would read as a color, a list separator and the start
of note text. Generated aliases are sanitized to alphanumerics, prefixed when
they would start with a digit, and suffixed when they collide with a name or
alias already in the diagram. A participant introduced only by a message has no
declaration to rewrite, so one is inserted at its first use, which is where
PlantUML already orders it.

References are rewritten only where PlantUML expects a participant: message
endpoints (before the colon, reusing ARROW_RE so a name like Web-Server is not
split down the middle), activate/deactivate/destroy/create, note placement and
ref over. Prose is returned byte-for-byte. Incoming names have their whitespace
collapsed, so a pasted newline cannot split the declaration, and a blank name
leaves the diagram alone rather than labelling a participant "".

Also fix the mismatch this exposes elsewhere: add_message, add_activation,
delete_activation, add_note and delete_participant all named participants by
their displayed name, so adding a message to `participant "Space Room" as
SpaceRoom` emitted `Space Room -> Bob` and deleting it left its notes behind.
They now use the reference name. The routes still receive displayed names and
resolve them server-side, so no frontend change and the note dropdown keeps
showing readable labels.

extract_participant_rect in the tests picked the first <rect> regardless of
type, so in a diagram with an activation bar it returned the bar and the
"clicked participant" resolved to whichever lifeline it sat on. It now filters
with is_participant_rect, as the backend does.
The rename field is a <textarea>, for layout consistency with the other modals,
so Enter inserted a newline into a name that occupies a single puml line: the
declaration split and the diagram failed to render.

Enter now submits, which is what a one-line name field is expected to do.
Ctrl+Enter already submitted, via the global handler, and still does.

The backend collapses whitespace in a submitted name regardless, so a name
pasted from several lines is handled as well.
Record the three renaming cases and the displayed name / alias / reference name
distinction they turn on, per the pre-commit checklist: CHANGELOG, FEATURES,
the editParticipantName, addNote and addActivation contracts, the glossary, the
architecture note on sequence data classes, and the steering files for the new
rename.py module and test files.
The declaration parser matched every lifeline keyword (actor, database, queue
and the rest), which bought nothing and cost correctness. Only `participant`
draws the rounded header rect is_participant_rect looks for, so no other
keyword reaches Diagram.participants and none can be clicked or renamed.

Worse, matching them let a declaration the editor cannot act on claim the line
of one it can: two lifelines may share a displayed name when their identifiers
differ, so an `actor "Alice" as A` beside a `participant Alice` was handed the
participant's slot, along with the actor's alias. A rename then rewrote the
actor and left the real participant alone, and hover-highlighting and box
creation pointed at the wrong line.

Narrowing the regex to `participant` fixes that as a side effect rather than as
a special case, and removes the keyword tuple, the second scan over the source
and ParticipantDeclaration.keyword (always the same literal once only one
keyword parses).

taken_identifiers is narrow as a result, so a generated alias can still
duplicate an identifier belonging to an aliased actor or database, which makes
PlantUML merge the two lifelines rather than report an error. Preventing that
needs a wider second scan for a collision that requires the new name's
PascalCase to match an aliased non-participant exactly; the limitation is
asserted in the tests instead, so it stays a decision rather than becoming a
surprise.

Also trims this branch's changelog entries, which had grown to two or three
times the length of the surrounding ones.
…l-extension/npm_and_yarn-56e86fd9c9

chore(deps): bump js-yaml from 4.3.1 to 4.3.2 in /plantuml-extension in the npm_and_yarn group across 1 directory
Enter now inserts a newline in the participant rename field instead of
submitting; on save the newline is folded into the literal \n escape
PlantUML draws as a broken label (participant "New\nName" as NewName).
Ctrl+Enter submits. Re-opening the modal shows the stored \n as a real
newline for editing.

Also tightens the surrounding rename/classes docstrings and comments.

def parse_participant_declaration(line: str) -> ParticipantDeclaration | None:
"""Split a puml line into declaration parts, or None if it is not one."""
match = PARTICIPANT_DECLARATION_RE.match(line.strip())

def _rewritten_endpoint(endpoint: str, spellings: set[str], new_token: str) -> str:
"""Replace one side of a message arrow, preserving activation shorthand."""
shorthand = _ENDPOINT_SHORTHAND_RE.search(endpoint.rstrip())
head = line if colon == -1 else line[:colon]
tail = "" if colon == -1 else line[colon:]

arrow = ARROW_RE.search(head)
dependabot Bot and others added 8 commits October 2, 2026 16:33
Bumps the npm_and_yarn group with 1 update in the /plantuml-extension directory: [undici](https://github.com/nodejs/undici).


Updates `undici` from 7.29.0 to 7.30.0
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](nodejs/undici@v7.29.0...v7.30.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 7.30.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps the uv group with 1 update in the / directory: [urllib3](https://github.com/urllib3/urllib3).


Updates `urllib3` from 2.7.0 to 2.8.0
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](urllib3/urllib3@2.7.0...2.8.0)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.8.0
  dependency-type: indirect
  dependency-group: uv
...

Signed-off-by: dependabot[bot] <support@github.com>
…l-extension/npm_and_yarn-e184a88377

chore(deps-dev): bump undici from 7.29.0 to 7.30.0 in /plantuml-extension in the npm_and_yarn group across 1 directory
…2 updates

Bumps the npm_and_yarn group with 2 updates in the /plantuml-extension directory: [brace-expansion](https://github.com/juliangruber/brace-expansion) and [markdown-it](https://github.com/markdown-it/markdown-it).


Updates `brace-expansion` from 2.1.2 to 2.1.7
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](juliangruber/brace-expansion@v2.1.2...v2.1.7)

Updates `markdown-it` from 14.3.0 to 14.3.2
- [Changelog](https://github.com/markdown-it/markdown-it/blob/14.3.2/CHANGELOG.md)
- [Commits](markdown-it/markdown-it@14.3.0...14.3.2)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 2.1.7
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: markdown-it
  dependency-version: 14.3.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
chore(deps): bump urllib3 from 2.7.0 to 2.8.0 in the uv group across 1 directory
Bumps the uv group with 2 updates in the / directory: [virtualenv](https://github.com/pypa/virtualenv) and [werkzeug](https://github.com/pallets/werkzeug).


Updates `virtualenv` from 20.36.1 to 21.7.13
- [Release notes](https://github.com/pypa/virtualenv/releases)
- [Changelog](https://github.com/pypa/virtualenv/blob/main/docs/changelog.rst)
- [Commits](pypa/virtualenv@20.36.1...21.7.13)

Updates `werkzeug` from 3.1.6 to 3.1.9
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst)
- [Commits](pallets/werkzeug@3.1.6...3.1.9)

---
updated-dependencies:
- dependency-name: virtualenv
  dependency-version: 21.7.13
  dependency-type: indirect
  dependency-group: uv
- dependency-name: werkzeug
  dependency-version: 3.1.9
  dependency-type: indirect
  dependency-group: uv
...

Signed-off-by: dependabot[bot] <support@github.com>
…l-extension/npm_and_yarn-0ade2630a2

chore(deps-dev): bump the npm_and_yarn group across 1 directory with 2 updates
chore(deps): bump the uv group across 1 directory with 2 updates
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants