Release Notes
All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to a single monorepo-wide version declared in global.json.
[Unreleased]
Fixed
- Core:
./gradlew publishToMavenLocalworks without a GPG key; signing is only required when publishing to Maven Central. The 0.x releases aren't on Maven Central (it has older versions up to 1.3.0), so the Core Java documentation now explains how to install a release locally.
[0.10.0] - 2026-11-13
Added
- Core: an
ActiveDialoguecan keep a log of the conversation, as the Web Service does (#483).setLoggedDialogue(log, factory)orkeepInMemoryLog()records every agent statement and reply, across dialogues (a reply into another dialogue completes the log and starts a new one from theLoggedDialogueFactory), and completes it when the dialogue ends.back()goes to the previous agent step,ActiveDialogue.restore(...)rebuilds a dialogue from a log at a step,executeCurrentNodeshows it again,cancel()marks the log cancelled, andgetCurrentMessage()says where the step is in the log.InMemoryLoggedDialogueis a simple log for an application that doesn't store it; the logs of one conversation share a session ID, and each points back to the one before it (getPrevious()). A log is set before the dialogue starts:ExecutableProject.createDialogue(name, language, variables)gives a project's dialogue without starting it. - Core:
ExecutableProject.load(Path)loads a project from itsdlb-project.xmlin one call. A project with parse errors fails with aProjectParseExceptionthat lists every error per file, instead of a project that isnull. Parse warnings are available from the newExecutableProject.getWarnings(), also for projects loaded withProjectParser(#473). - Core:
ExecutableProject.getDialogue(name)andgetDialogue(name, language)look up a dialogue by its name. They use the same language rules as the Web Service: the exact language, otherwise the source language. An unknown dialogue fails with anExecutionExceptionof typeDIALOGUE_NOT_FOUND.getDialoguePointer(name, language)returns the dialogue'sResourcePointer, andgetSourceLanguage()returns the project's source language, also for a project without metadata (#473). - Core: an
ActiveDialoguefollows a reply into another dialogue itself. Create it with aDialogueResolver, which finds a dialogue by name and language (anExecutableProjectis one), and callselectReply(replyId, eventTime): it returns the next node, also when that is in another dialogue, keeps the sameVariableStoreand follows the user's gender there too. ADialogueSwitchListenerhears of the switch before the first node there runs (#473). - Core:
ExecutableProject.startDialogue(name, user)starts a dialogue of the project for a user, with a newVariableStore. Overloads take a language, an existingVariableStoreand the start time. The dialogue follows replies into the project's other dialogues, and where a translation depends on the user's gender, it follows$dlb.userGenderat every step (#473). - Core:
ActiveDialogue.getCurrentMessage()returns the current step as aDialogueMessage, the same shape the Web Service sends, ready to show: speaker, statement and replies.ActiveDialogue.isFinished()says whether the dialogue has finished: after a reply toEnd, or at a node without replies (asked of a node that hasn't been executed yet, it throws anIllegalStateException, asgetCurrentMessage()does). The Web Service and the command-line tool use this one rule now (#473). - Core:
ExecutableProject.generateSummaryString()describes a loaded project, asProjectParserResult.generateSummaryString()did for a parse without errors (#473). - Core:
DialogueStatement.getText()returns the text of a statement (its text segments joined, without input fields or actions), asfullStatement()does in the JavaScript client.ReplyMessage.getText()does the same for a reply, andReplyMessage.isAutoForward()tells an auto-forward reply apart. The JSON the Web Service sends is unchanged (#485). - Core:
ActiveDialogue.selectReply(replyId),selectReply(reply)(aReplyMessageof the current step),startDialogue()andstartDialogue(nodeId)need no time: they use now, in the time zone of the user of the dialogue'sVariableStore(#485).
Changed
- Internal API: Core:
DialogueStateis removed (#483). AnActiveDialoguerestored withActiveDialogue.restorecarries the log and the step itself, and the Web Service uses that instead of its own code for recording, going back and resuming; its REST behaviour, JSON and database are unchanged. - Core: Breaking: an
ActiveDialoguealways has aDialogueResolver, and follows replies into other dialogues itself. The constructorActiveDialogue(ResourcePointer, Dialogue, VariableStore)is replaced byActiveDialogue(DialogueResolver, ResolvedDialogue, VariableStore), or start a dialogue withExecutableProject.startDialogue.processReplyAndGetNodePointerandprogressDialogue(InternalNodePointer, ...)are replaced byselectReply(replyId, eventTime), which returns the next node instead of a pointer, also in another dialogue.setUserGenderTranslationsis replaced by the user-gender translations of theResolvedDialogue(#473). - Core: the command-line tool (
DialogueBranchCLI) plays a dialogue that isn't available in the chosen language in the source language, also when a reply links into it, like the Web Service does. It used to stop with an error. For a language the project doesn't have at all,--executeprints a warning and plays the source language (#473).
Fixed
- Web service:
/dialogue/progressand/dialogue/backwith a step that is not in the dialogue's log answer404, instead of a generic500(#483). - Web service:
/test-session/progresswith a reply that is not in the current step answers404, instead of a generic500. - Core:
ActiveDialogue.selectReplythrows anExecutionExceptioninstead of aNullPointerExceptionfor a reply that is not in the current node (REPLY_NOT_FOUND) or a dialogue that has finished (DIALOGUE_COMPLETED), and refuses a reply once its log has been cancelled or completed, instead of recording into it; it changes nothing then. - Studio: after a reply that led into another dialogue, the Dialogue Workspace kept showing the log ID of the first dialogue, and the Dialogue Browser could open an ongoing dialogue in a second tab. It now follows the new dialogue's log (#473).
- Core: the command-line tool kept no variables when a reply led into another dialogue: a variable set in one dialogue was empty in the next. It now plays them as one run, with the same variables throughout (#473).
[0.9.2] - 2026-11-11
Added
- Web service:
GET /variables/list-projectalso names the dialogues that read and write each variable, in two new fieldsreadByandwrittenBy, so a client can show the variables of one dialogue. The new fields are added to each entry, the existing ones are unchanged.DialogueBranchAuthoringClient.listProjectVariablesreturns them too.
Changed
- Web service:
GET /variables/list-projectis now a stable end-point, covered by the compatibility policy, and documented with the other variable end-points. A service that needs to know which dialogues set which variables can now build on it.
Fixed
- Web service:
GET /variables/get,POST /variables/set,POST /variables/set-single,GET /dialogue/list-dialoguesandGET /dialogue/get-ongoinganswer a project that doesn't exist with a 404 (Project not found: <slug>), like the other project end-points. The variable end-points answered a 500, and the dialogue end-points an empty answer (#467).
[0.9.1] - 2026-11-04
Security
- Web service, BFF and mock variable service: upgraded to Spring Boot 4.1, which fixes critical vulnerabilities in Tomcat, Spring MVC and Spring Security.
- Studio: updated
dompurify,vueandvite, and the Docker image now serves the build with nginx on Alpine instead of Apache httpd on Debian.
[0.9.0] - 2026-11-04
Added
Core and web service: a project can define its speakers, each with a name, a gender (
male,femaleorundefined) and whether it is the default for new nodes (#393, #394). They are an optional<speakers>section indlb-project.xml, which core reads and writes (ProjectMetaData.getSpeakers(),Speaker), refusing an empty, duplicate or reserved (_user) name, an unknown gender or more than one default. The web service keeps them as a draft and per published version, like translation languages:get-projectreturnsdraftSpeakersandlatestVersion.speakers, publishing snapshots them, export and import carry them, and discarding all draft changes restores them. A new database migration (V12) adds the tables. Internal API:POST /project/update-drafttakesspeakers, the project's complete new list of speakers.Studio: the project's speakers in Configure Project and the node editor (#395, #396). A new Speakers tab adds and removes speakers, sets their gender and the default speaker, saved with the rest of the project's draft configuration. When a project has speakers, the node editor picks a node's speaker from them (keeping, and marking, one that isn't), a new node gets the default speaker, and a node other than
Endcan't be saved without a speaker. Internal API:DialogueBranchAuthoringClient.updateProjectDrafttakesspeakers.Core, web service and Studio: a warning for a node whose speaker isn't one of the project's speakers (#397). Core's
ProjectParserreports it with its other warnings when the project defines speakers (a new constructor takes them for a loader without adlb-project.xml); it never stops a project from parsing, publishing or running.POST /publish/verifynow also returns thewarningsper dialogue, which it dropped before, such as orphaned nodes. Studio's publish wizard lists them, and the graph editor and node editor mark a node with an undefined speaker.Web service and Studio: renaming a speaker renames it in the project's nodes and translations too (#398). In Configure Project, a rename is saved with the rest of the project's draft, after confirming how many nodes, dialogues and translations it changes. Internal API: a
speakersentry ofPOST /project/update-drafttakespreviousNameto rename that speaker; the newGET /project/find-speaker-referencescounts what a rename changes;DialogueBranchAuthoringClient.findSpeakerReferences.Core and web service: translations use the speakers' genders (#399). A
maleorfemalespeaker of the project selects itsmale_speaker/female_speakertranslations, in live playback and in tests; anundefinedgender (now also the default) selects a gender-neutral translation where there is one, else the speaker's first. Core:TranslationContext.Gender.UNDEFINED,TranslationContext.forSpeakers.Core and web service: the end user's gender, the reserved variable
$dlb.userGender(male,female,undefinedor empty), selectsmale_addressee/female_addresseetranslations, the newmale_user/female_userones (the user wherever they are in the text, also when only talked about), and those of the user's own replies (#400). It is read at every step, in live playback and in tests; a dialogue whose translations depend on it is translated for each gender when first needed. Core:ExecutableProject.getDialogueForUserGender,ActiveDialogue.setUserGenderTranslations.Studio: gender-specific translations (#453, #454, #455). The Translation Editor adds a Female user and a Male user translation to a text (stored as
female_user/male_user), removes a gender variant in every language, shows each speaker's gender, and marks a variant that can never be used. Test in [language] as [any user / a female user / a male user] sets$dlb.userGenderin the test values.
Changed
- Breaking: variable names starting with
dlb.are reserved for variables with a predefined meaning (#400). Setting one that isn't defined, or a defined one to a value it doesn't allow, is refused: a<<set>>with a fixed value, or an input command storing into one, is a parse error;POST /variables/set-single,POST /variables/set, a reply's input and the test variables answer400; a value from the External Variable Service is skipped and logged. Rename any variable of your own that starts withdlb.. Core:ReservedVariables;VariableStorethrowsIllegalArgumentExceptionfor a refused value. - Breaking: the reserved node header tag
colorId(and its legacy spellingcolorID), a palette index, is replaced bynodeColor, a colour written as#rrggbb(#203). Studio's node editor offers the same ten presets plus any custom colour. Existing content isn't converted: an oldcolorIdtag is kept as an ordinary custom tag, and the node shows the default colour until it is given one. Dialogues run as before (the tag never affected execution). To keep the old colours in.dlbfiles, replacecolorId: N(orcolorID: N) withnodeColor:and the Nth of#c2410c,#0d9488,#7c3aed,#0369a1,#be123c,#65a30d,#a16207,#4f46e5,#0891b2,#be185d(counting from 0).
Fixed
- Studio: moving several selected nodes together in the graph editor saves every node's new position, not only the one that was dragged (#196); the others went back to where they were the next time the dialogue was opened.
- Studio: saving Configure Project no longer replaces the project's name in the header with its slug.
[0.8.1] - 2026-10-30
Fixed
- Web service: requests of the same user that arrive at the same time, such as a double tap, a retry racing the original or two open tabs, no longer overwrite each other (#284, #216). Two steps of one dialogue could each save over the other, losing one, and a dialogue step that changes a variable, such as
<<set $count = $count + 1>>, could lose a change. Starting, progressing, continuing, going back in and cancelling a dialogue, and setting variables, now run one at a time per user and project; other users and reading aren't held up. - Web service: the first requests of a new user arriving at the same time could fail with a
500error and leave the user with two diverging in-memory sessions (#214); a user now always gets one. - Web service: creating, changing or deleting a draft node or translation saved its content and then marked the dialogue as changed in two separate steps, so a failure between them left the dialogue's Draft status wrong (#217); both now happen together or not at all.
- Web service and Studio: importing a project from a zip made with macOS Finder's Compress failed validation (#444). The import now leaves out what an operating system adds to an archive for its own use (everything under
__MACOSX/,._files,.DS_Store,Thumbs.dbanddesktop.ini), and also finds the project when the archive holds nothing but one folder with it, as when you compress the project's folder rather than its contents.
[0.8.0] - 2026-10-30
Added
- Studio and web service: Studio shows whether the configured External Variable Service is running (#430). The status bar says "External Variable Service: connected" or "not reachable" (nothing when none is configured), and admins find its URL, last successful and failed calls, last error and failed calls in a row in Technical Information. The Variable Browser's Used list also says when the service can't be reached. Internal API: a new
GET /info/external-variable-service(editors and admins) checks the service with oneGET /variables/supportedcall, at most every 30 seconds; any answer but a401or403counts as running./info/technicalgainsexternalVariableService, and client-js gainsgetExternalVariableServiceStatus. - Studio: the Variable Browser shows the hints an External Variable Service gives for its supported variables (#434). In Used, a variable shows its type and a lock when it is read-only, with its description and allowed values in the tooltip. In Values and Test values, a lock marks a read-only variable, and its tooltip says what changing it does: only the test values change, or the service's value replaces the change at the next dialogue start.
- Web service, client-js and mock-variable-service: an External Variable Service can give each variable it reports in
GET /variables/supportedthe optional hintstype(number,stringorboolean),values(the allowed values) andreadOnly(#433).GET /variables/list-supportedpasses them on as sent, and client-js'slistSupportedVariablesnow returnsSupportedVariableobjects (also exported from@dialoguebranch/client-js/protocol) carrying them. The hints are for authors only and aren't enforced.mock-variable-servicedeclares its three variables as read-only strings, withdayPart's allowed values, and adds fourexample.*variables that show each hint once; it keeps their values per user and project in memory, starting with a random value of the declared type and changing when a dialogue changes them. It now only answersretrieve-updatesfor the variables it reports as supported: it no longer returns other variables half of the time with their value unchanged. - Studio: a search field on the project page, filtering projects by name, slug or description.
- Studio, web service and client-js: simulate a user, to investigate a problem they have without changing any of their data (#411). Set Delegate User now offers Simulate as (the default) and Act as. While simulating, dialogues run as test sessions against a copy of the user's variables, on the published content in Live Mode and on the draft in Authoring Mode, with a notice and Stop simulating; the user's real ongoing dialogue isn't offered to resume. Switching between Live and Authoring Mode now stops simulating or acting as someone.
POST /test-session/starttakescontent=publishedto run a dialogue's published version; client-js'sstartTestSessiontakescontent. - Studio, web service and client-js: deleted projects whose users' data was kept are listed for admins in a separate, collapsed Deleted projects section on the project selection page, with how much users' data they still have, and can be deleted permanently from there, user data and all, after typing the slug to confirm (#403). New
GET /project/list-deleted-projectsandPOST /project/purge-deleted-project; client-js gainslistDeletedProjectsandpurgeDeletedProject. - Web service and client-js: an admin can erase everything stored about a user — their variables, variable history and dialogue logs in every project, and the user itself — with
POST /users/erase-user(new admin-onlyUSER_ERASEpermission); a configured External Variable Service is sentnotify-clearedfor each project the user had variables in (#320). client-js gainseraseUser. - Studio: a Users page for admins, opened from the project selection page, lists everyone the web service has seen with how many variables and dialogue logs it holds for them, and erases a user's data after typing their username to confirm (#407).
Changed
- Documentation: a new Compatibility Policy page lists which surfaces are stable (the
.dlblanguage, the stable REST end-points, authentication, the External Variable Service protocol,DialogueBranchClient, configuration and upgrades,dlb-core-java's public API) and which are internal to Studio. From here on, only a change to a stable surface is labelled Breaking:; a change to an internal one is labelled Internal API:. Release notes up to 0.7.0 are unchanged. - Breaking: Language: variable names may contain dots, so
$settings.themeis one variable (#432). A dot belongs to the name only when a letter or underscore follows it, so$score.at the end of a sentence is still a variable followed by a full stop. The rule is the same in text, replies, quoted strings and<<set>>/<<if>>expressions. As a side effect,$m.keyin an expression no longer reads a field of a map held in$m(an undocumented behaviour of the expression engine) but the variablem.key. The Web Service's variable end-points accept the same names (and now also a leading underscore, as scripts always did). Indlb-core-java, the rule iscom.dialoguebranch.model.common.VariableNames. - Breaking: Web service: the Web Service authenticates to the External Variable Service with an OAuth2 access token instead of a static API key (#428). It gets the token from Keycloak with the client-credentials grant, caches it and fetches a new one shortly before it expires, and sends it as
Authorization: Beareron every call. Thedlb.external-variable-service.api-keysetting is gone; new settingsdlb.external-variable-service.oauth2.token-uri,client-id,client-secretand optionalscopereplace it, and the Web Service does not start without them while the integration is enabled. An External Variable Service must check the token's signature, issuer, expiry, audience and role, as described on its documentation page;mock-variable-servicedoes, and the Docker stack's realm has the clients and role for it. Failing to get a token is handled like any other failed call and never breaks a dialogue. - Breaking: Web service: a variable updated by the External Variable Service is stored as updated when the Web Service received it, as for every other source, so
/variables/getreturns that time as itsupdatedTime(#321). Theretrieve-updatesresponse now carries each variable'snameandvalueonly; anupdatedTimeorupdatedTimeZoneit still sends is ignored. The request still sends each variable'supdatedTime, which now means the Web Service has held that value since then.mock-variable-serviceanswers with name and value only. - Web service: a new project's starter dialogue has a preamble saying it was generated automatically.
- Studio: Live Mode can only be chosen once a project has a published version; an unpublished project opens in Authoring Mode.
- Studio: the header's mode switch shows Live and Authoring side by side and switches in one click, and a mode bar under the header says which mode you're in, in its colour (amber for Live, green for Authoring). Simulating or acting as another user is shown in the mode bar too, which turns red while acting as them, instead of in a banner above the dialogue.
- Studio: the Dialogue Workspace's view switch is in its title row, with a label on each view (Test (speech bubbles), Test (text style), Edit Dialogue, Translate Dialogue), and its other controls are in a toolbar above the tabs. Opening Edit Dialogue no longer collapses the Variable Browser, and the workspace's own reset of the test values is gone: the Variable Browser has one.
- Studio: the Dialogue Browser can be collapsed, like the Variable Browser; both fold away with a button on their edge next to the Dialogue Workspace, and stay collapsed after a reload.
- Studio: the Dialogue Browser, Dialogue Workspace and Variable Browser line up, with restyled panel titles and a grey toolbar row at the top of each panel; the Dialogue Browser's and Variable Browser's buttons, view switch and sort order moved into their toolbars, with sorting in a small menu.
- Studio: a refreshed look for the participant view and the project and user pages: a shared header with the logo, a "Studio" wordmark, a divider and a greeting; for editors and admins a menu bar to switch between Project Management and User Management (admins only), replacing the Users and ← Projects buttons; and the page content in a white box with rounded corners. The Service Unavailable, Update Available and Insufficient Privileges screens use the same logo.
- Studio: acting on behalf of another user for real (Act as) is treated as the powerful operation it is (#330): choosing it warns that the user's dialogue logs, variables and history change as if they acted themselves, the confirmation says Act as <user>, and Studio shows in red while acting, with Stop acting as. Set Delegate User is shown to admins only.
- Docs: the Authentication page no longer describes a trusted server that acts for all of its end users through one
adminaccount anddelegateUser(#330). A backend serving many users should forward each user's own access token instead;delegateUseris for an administrator's testing and support work, described in a new section on acting on behalf of another user. - Web service: test sessions read from the External Variable Service like real playback does (#411): at every dialogue start, also through a reply into another dialogue, with the values of the session's copy of the variables. The answers go into the copy only, and a test session still never notifies the service. Every
retrieve-updatesrequest now carriestestSession(trueorfalse), so an External Variable Service can tell test sessions from real playback; the External Variable Service page explains where requests for updates come from. - Internal API: Web service, client-js and Studio: testing a dialogue no longer changes the user's real variables (#319). A test session runs against the tester's own copy of the variables, kept in memory and seeded from the stored ones: it is never saved, recorded in the variable history or sent to the External Variable Service. All of a tester's test sessions in a project share one copy, which
GET /test-session/variables,POST /test-session/set-variableandPOST /test-session/resetread, change and reset. Testing as another user (delegateUseronstart) only reads their variables. The end-points are renamed from/draft/*to/test-session/*(draftSessionIdbecomestestSessionId,revert-variablesis replaced byreset, andprogressandcancelno longer takedelegateUser), and the permissionDIALOGUE_DRAFT_TESTbecomesDIALOGUE_TEST. In client-js,startDraftDialogue,progressDraftDialogue,cancelDraftDialogueandrevertDraftVariablesbecomestartTestSession,progressTestSession,cancelTestSessionandresetTestVariables, plusgetTestVariablesandsetTestVariable. In Studio's Authoring Mode, the Variable Browser's Test values shows and edits the test copy, with a button to reset it to the stored values. - Breaking: Web service and Studio: deleting a project removes its dialogues, translations and version history, but keeps its users' variables, variable history and dialogue logs by default (#315). The project stays as a deleted project (no longer found or listed) under the slug
<slug>__deleted, with its logs moved along, so the original slug is free right away.POST /project/delete-projecttakesdeleteVariablesanddeleteLogs(bothfalseby default) to delete that data too; with both, the project is removed for good. Studio's delete confirmation offers both as unticked options. client-js'sdeleteProjecttakes them as options. A new database migration (V11) adds the deleted flag. - Internal API: Web service and client-js:
GET /usersalso lists users with no known username (only ever seen as a delegate,usernameisnull), after the named ones; itsusernamefilter is replaced bysearch, which matches the username or the subject; and each entry hasvariablesanddialogueLogscounts (#407). client-js'slistUserstakessearchinstead ofusernameFragment. Studio's delegate user picker finds these users too.
Fixed
- Core: a translated node no longer shows a variable before a
<<set>>that comes before it in the source (#437). Translating moved every<<set>>and<<action>>behind the translated text, so the text showed the variable's old value. A command before the text now keeps its place, and text with no translation keeps its source order exactly. A command in the middle of a translated sentence still follows it, since a translation can't say where inside its own text the command belongs. - Web service: a slow or unreachable External Variable Service no longer breaks or stalls a dialogue (#219). Every call to it now has a connect and a read timeout, new settings
dlb.external-variable-service.connect-timeout(default2s) andread-timeout(default5s). A failedretrieve-updateslets the dialogue start with the values it has, and a failednotify-updatedornotify-clearedis logged while the change, clear or erase stays in effect; before, a failed notification could throw in the middle of a dialogue turn, and a hanging service could block it indefinitely. - Web service: a request the caller got wrong is answered with its own 4xx and the reason, instead of a generic
500 Internal Server Error(#220): a missing required parameter or upload part (400), a parameter of the wrong type (400), an unsupported content type (415) or method (405) and an upload that is too large (413). An upload that arrives without its file, for example because a proxy didn't pass the body on, now says so. Real server errors still answer500, without details. - Studio: the Documentation link in the user menu pointed to a page that no longer exists; it now opens the documentation site.
- Studio: deleting, adding or renaming a dialogue collapsed every folder in the Dialogue Browser; open folders now stay open.
- Studio: a long page, such as the Users list, also got a horizontal scrollbar.
- Studio: blank lines between paragraphs of a dialogue's script comment (its preamble) were dropped on save; they're now kept, as bare
//lines. - Studio: clicking outside a dialog, such as New Project or the node editor, closed it, losing what was typed; dialogs now close only with their close button or Cancel.
- Studio: when the participant view's dialogue doesn't exist, a participant now gets a plain message instead of a technical error with a Try Again button that couldn't help.
- Web service and Studio: test sessions stayed in memory, each with its own parsed copy of the project, until the web service restarted, unless they were cancelled (#218). A test session now ends when its dialogue does, and sessions idle for longer than
dlb.test-session.idle-timeout-minutes(default 30), and copies of the variables no session uses, are removed. Studio says so when a test has expired, with a button to start it again. Two requests on the same test session at once could also corrupt it; a session now handles one request at a time (#215). - Web service: a request made on behalf of another user (
delegateUser) that runs a dialogue or sets variables overwrote that user's time zone with the caller's, so the user's own next dialogue steps and variable changes were stamped in the wrong time zone (#331). It now leaves the user's time zone alone and stamps what it changes in it; itstimeZoneparameter is ignored. - Web service: deleting a project failed as soon as any user had a variable in it (#315); a new project with the slug of a deleted one could inherit its users' cached variables (#316) and its dialogue logs, including an ongoing dialogue to resume (#317); and a deleted project stayed listed and playable until the web service restarted. Deleting a project now succeeds, unloads it, and keeps its users' data away from a new project with the same slug.
- Studio: a delegate user set in one project stayed active, without being shown, after closing it and opening a project again; opening a project now clears it (#407).
[0.7.0] - 2026-10-15
Added
- Studio: delete a dialogue (#390). Delete… in a dialogue's menu in the Dialogue Browser (Authoring Mode) marks it for deletion after a confirmation that warns how many replies in other dialogues link to it; it can be restored until the next publish, and its open tabs close. Studio had no way to delete a dialogue before, although the web service supported it.
- Studio, web service and client-js: discard all of a project's draft changes (#364). Discard All Draft Changes… in the Project menu makes the whole draft identical to the latest published version again: every dialogue goes back to its published content and name, dialogues that were never published are removed, dialogues pending deletion are restored, and the project's name, description and translation languages go back to the published ones. The new
POST /project/discard-draft-changesneeds the permission to publish, and refuses a project that was never published. client-js gainsdiscardAllDraftChanges. - Studio, web service and client-js: discard a draft dialogue's changes (#363). Discard changes on a dialogue marked Changed in the Dialogue Browser (Authoring Mode) makes its draft identical to its published version again: nodes and their layout, preamble, translations, and its published name if it was renamed; it is then no longer marked as changed. The new
POST /authoring/discard-dialogue-changesrefuses a dialogue that was never published or is pending deletion, a published name another dialogue has taken, and a translation language renamed since publishing. client-js gainsdiscardDraftDialogueChanges.
Changed
- Studio: the Dialogue Browser's dialogue rows show only their ▶ (open) button; the other actions (Open, Rename…, Export as .dlb, Discard changes…, Delete…) are in a context menu, opened by right-clicking a row or with a ⋯ button shown when pointing at it (always shown on touch screens) (#389). The menu, also used for the node editor's Insert statement menu, works with the keyboard and stays inside the window. A dialogue pending deletion keeps its Restore button.
[0.6.0] - 2026-10-15
Added
- Studio: the Translation Editor shows translations it used to hide (#386). A translation stored under a source text that differs from the dialogue's text only in whitespace now shows in its row, and editing it replaces it instead of adding a second entry; gender-specific translations (nested context groups such as
female_addressee) show as editable sub-rows under their text. - Studio: the Translation Editor shows a dialogue's stale translation entries and lets you delete them (#375). Entries that match no text of the dialogue on speaker and text (for example after the text changed) are never used and were invisible; a Stale entries (N) button now lists them per translation language, with their speaker and any gender contexts, and deletes one, or all of a language's, after a confirmation.
- Studio, web service and client-js: import translation files (
.json) into a translation language of an existing project, as draft translations (#340). Import Translations in the Project menu (Authoring Mode) matches each file to the existing dialogue named after the chosen folder and the file name, shows which already have a translation in the chosen language, and only replaces those when asked; open Translation Editors of those dialogues reload. The newPOST /authoring/import-translationsimports all or nothing, refuses files that aren't valid translation files (the check ofupdate-translation, #376), contain no translations, or have no dialogue (or one pending deletion), stores each file re-serialised, and reports per file how many entries match no term of the dialogue (by speaker and text, kept anyway) and how many terms stay untranslated.list-dialoguesnow lists, per dialogue, the translation languages it already has a translation in (translatedInto). client-js gainsimportTranslations. - Studio, web service and client-js: export a single dialogue as a
.dlbfile (#341). Export as .dlb on a dialogue in the Dialogue Browser (Authoring Mode) downloads its current draft: the preamble and nodes as an author sees them, not the published version, without translations. The file is named after the dialogue without its folder, and imports again with Import Scripts as the same dialogue. The newGET /authoring/export-dialogueneeds theeditororadminrole; client-js gainsexportDialogue. - Studio: the Variable Browser's History view can be filtered by a date range (#371). Pick a Start date and an End date (whole days in your time zone, the one the entries are shown in; the end date is included), or use a preset: today, the last 7 days or the last 30 days. It combines with the exact-variable-name filter and Load more, and a start date after the end date is reported in the panel instead of being sent.
- Web service and client-js:
GET /variables/historyaccepts optionalfromandto(epoch milliseconds) to return only the changes recorded in the range[from, to)(#352).fromis inclusive andtoexclusive, so consecutive ranges never overlap or leave gaps; either can be left out, and they combine withvariableNameand paging. Afromafterto, or a value that is not a whole number, answers400. A new database migration (V10) adds indexes for the range lookup.DialogueBranchClient.getVariableHistoryacceptsfromandto(a number or aDate). - Studio and web service: import
.dlbscripts into a folder of an existing project, as draft dialogues (#339). Import Scripts in the Project menu (Authoring Mode, next to Import Translations, #340) previews the resulting dialogue names, shows which already exist, and only replaces those (keeping their translations, and restoring one pending deletion) when asked; open tabs of replaced dialogues reload. The newPOST /authoring/import-scriptsimports all or nothing, refuses files that aren't UTF-8.dlbfiles of at most 1 MB, don't make a valid dialogue name, or can't be stored as draft nodes, and reports syntax errors per file without refusing them. client-js gainsimportScripts.
Changed
Breaking: Core: a translation entry only applies to text of its own speaker (#377). The
Translatorused an entry of any speaker when the speaker had none for a text, and, through a bug in its gender filter, even when it had one, so two speakers saying the same text got the same translation. A translation file's top-level keys are now read as speaker names as a whole (multi-word names such asMartin McOwlwere split into separate words that never matched), nested context groups add to their speaker's entries instead of replacing the speaker, and an entry outside a speaker group is a parse error. A text without an entry of its own speaker stays in the source language. Indlb-core-java,ContextTranslationgains aspeakercomponent. The translation file format is now documented in the project reference.Breaking: Language: the speaker name
_useris reserved (#383). Translation files use it for the user's replies, so a node withspeaker: _usercollided with them; the parser now refuses it (except on anEndnode, whose speaker is ignored), and Studio's node edit panel refuses it before saving. Indlb-core-java, the reserved name isDialogueBranchConstants.DLB_USER_SPEAKER, which replacesSourceTranslatable.USER.
Fixed
Core: a translation containing a command other than
<<input>>(for example<<if>>or<<set>>) madeTranslationParsercrash instead of reporting a parse error (#379), so loading, verifying, publishing or draft-testing a project with such a translation failed with an internal error. It is now reported as an invalid translation string, naming the command.Web service:
update-translationstored any content it received, so invalid content was only noticed later, when testing a dialogue or publishing the project failed (#376). It now refuses content that isn't a valid translation file, including entries outside a speaker group, with the problems listed, and stores valid content re-serialised, without empty translations.Studio: importing a project archive (Import Project) failed, because the BFF passed the upload on to the web service without its contents (#339). The BFF no longer handles multipart requests itself (its CSRF check made the servlet container read the request body first), so uploads now reach the web service unchanged.
Studio: a collapsed dialogue preamble now leaves a small comment icon on the graph canvas. Hover shows the preamble; clicking expands it again (#361).
[0.5.0] - 2026-10-14
Changed
Breaking: Web service: the authoring end-points exchange a draft node's header as tags instead of text (#350).
list-nodes,create-node,update-nodeandrename-nodereturn a node as{id, title, tags, body, createdAt, updatedAt}, wheretagsis an ordered list of{key, value}pairs, instead of with aheaderstring.create-nodeandupdate-nodetake{tags, body}:create-nodeno longer has a separatetitle(the node's title is itstitletag, which must be a valid node name), andupdate-nodereplaces all of a node's tags. Tags that don't form a valid header (an invalid key, a key used twice, a value on more than one line) are refused with a400, and so is a script whose node headers aren't valid tags when it is stored as draft content (for example in a project import). In client-js,createDraftNode(projectSlug, dialogueName, tags, body)andupdateDraftNode(projectSlug, dialogueName, nodeTitle, tags, body)take tags, andrenameDraftNoderesolves to{node, referencesUpdated}. Studio no longer reads or writes header text itself.Breaking: Language: a node header is now a model, a list of
key: valuetags and nothing else (#343).//no longer starts a comment in a header: it is part of the value, soimage: https://...needs no escape there. A trailing comment on a header line (title: Start // entry point) now makes the value invalid, and a comment line inside a header is an error ("Comments are not allowed in a node header"); document a node with acommenttag or a body comment, and a script with its preamble. Header keys must consist of letters, digits,_and-. Body comments are unchanged. Header reading and writing is defined once, in core's newDialogueBranchHeader, which the parser and the Web Service use. Existing stored content is converted automatically: a migration removes header comments from draft and published dialogues (the comment lines at the start of a dialogue's first draft node move to its preamble), which doesn't change what any dialogue does.Breaking: Web service:
/authoring/create-nodeand/authoring/update-noderefuse (400) a header whosetitleisn't the node's title, or that has none (#343). A node's title changes only through/authoring/rename-node, which keeps its title and header in step.
Added
Studio: a dialogue's preamble (the comment block at the start of its script) is shown at the top of the graph editor, collapsible, and can be edited there as plain text: Studio adds and removes the
//prefixes, and empty lines are left out (#346). Dialogues without one offer "Add script comment". Web service:/authoring/list-dialoguesincludes each dialogue'spreamble, and a newPOST /authoring/update-preamblereplaces it (refusing lines that aren't//comments); client-js gainsupdateDraftPreamble.Studio: a node's
commenttag can be edited in the node edit panel, and a node with a comment shows a speech-bubble icon on its card in the graph editor, with the comment on hover (#348). Emptying the field removes the tag.Language: a reserved
commentheader tag, a one-line note about a node for authors (#347). It has no effect on how a dialogue runs, isn't sent to clients playing a dialogue, and isn't translated. Studio shows and edits it (see below).Language: a script's preamble, the
//comment lines at the very start of a.dlbfile, before the first node (#345). It belongs to the dialogue rather than to its first node: core'sDialoguemodel andDialogueBranchScriptSplitterexpose it, and the Web Service stores it with the draft dialogue (a newdraft_dialogues.preamblecolumn) and keeps it at the top of the script when publishing, exporting and importing. Previously such comments ended up in the first node's header in Studio's drafts, where moving that node in the editor deleted them.Core:
DialogueBranchScriptSplitter, which splits the text of a.dlbscript into its raw nodes (header and body text) and its preamble, and joins them back, by the same separator rules asDialogueBranchParser(#342). It accepts any text and never fails; deciding whether the result is a usable dialogue is left to the caller.
Fixed
Studio: moving a node in the graph editor, or saving it in the node edit panel, deleted the comments in its header, and a header value containing
//was cut off there (#343). Headers no longer contain comments, and Studio keeps//as part of a value.Web service: importing a project (and seeding one at startup) could store a valid
.dlbscript wrongly as draft nodes, without any error (#342). Scripts with Windows (CRLF) line endings, or with whitespace or a//comment on a---or===line, came out with no nodes at all, or with several nodes merged into one; a script without a final newline kept a stray===in its last node. Draft nodes are now split by the same rules the parser uses, and a script that still can't be stored as draft nodes (a node without a---line or atitle) is rejected with the dialogue name and line number, instead of nodes being dropped silently.
[0.4.0] - 2026-10-13
Added
- Web service: a new
supervisorrole (#326) for users who follow other users' progress without administering anything. It has everythingparticipanthas, plus a newUSER_DELEGATE_READpermission: reading another user's variables, variable history and dialogue logs throughdelegateUseron the read-only end-points (/variables/get,/variables/history,/log/get-session,/log/verify-id). It can never act on another user's behalf; that still requiresUSER_DELEGATE(admin), which now also includesUSER_DELEGATE_READ. The bundled Keycloak realm import gains thesupervisorclient role.
Fixed
- Web service: starting a dialogue in a
languagethe project doesn't have (e.g.nlfor a project'snl-NL) still runs it in the project's source language, as before, but now logs a warning naming the requested and available languages, so the mismatch no longer goes unnoticed (#335). Previously the language check never ran at all for projects loaded from the database. - Studio: the participant view (for users with only the
participantrole) failed to start its dialogue with a500for every user, because it starts it without alanguage, which/dialogue/startrequired (#333)./dialogue/start'slanguageis now optional: left out or empty, the dialogue starts in the project's source language. - Web service: a delegated read (
/variables/get,/variables/history,/log/get-session,/log/verify-idwith adelegateUsernaming another user) no longer changes anything about that user. It used to overwrite their in-memory time zone with the caller's, so their next dialogue steps and variable changes were stamped in the wrong time zone (#327), and it created ausersrow for any subject named, including mistyped ones, while returning empty results (#328). It now reads stored data directly, and answers404with codeUNKNOWN_USERfor a user the service doesn't know.
[0.3.0] - 2026-10-11
Added
- Web service: Dialogue Branch Variable value history (#259). Every change to a user's variables (from a
<<set>>, an input reply,/variables/set*, or the External Variable Service) is now also appended to a newvariable_historytable (migrationV7), one row per variable per change, withnullrecording a removal. Read it with the newGET /variables/historyend-point (projectSlug, optionalvariableName,page/pageSize; newest first), gated by a newVARIABLE_READ_OWN_HISTORYpermission held by every role. Each entry is stamped with the Web Service's own clock at the moment of recording. Each entry also carries anid, increasing in recording order, so a client paging through the history can de-duplicate entries when new ones shift the pages (#323). - Client JS:
DialogueBranchClient.getVariableHistory({ projectSlug, variableName, page, pageSize })reads the newGET /variables/historyend-point, returningVariableHistoryEntryinstances (new model type, also exported from./protocol) (#323). - Studio: a History mode in the Variable Browser (Values · Used · History) (#323), listing every recorded change to the current user's variables in the project, newest first, with "Load more" paging. Each entry shows the value (or removed), its source, and when it was recorded, in the Studio user's own time zone. Search and sort apply: a search naming a known variable exactly is answered by the server, so that variable's full history pages correctly; partial text filters the loaded entries, with a hint while older ones aren't loaded yet. Sort is newest/oldest first. Clicking a name narrows the list to that variable. It follows the delegate user and refreshes after every dialogue step and variable change, and is only requested while History is showing.
Changed
- Breaking: Core:
VariableStoreChange.Clearnow carries the names of the variables it removed (getVariableNames()), likeRemovealready did, so a listener can tell what a clear removed (#259). Its constructors take that collection as a new first argument:Clear(Collection<String>, ZonedDateTime[, VariableUpdatedSource]). - Breaking: Core: the
Commandhierarchy (ActionCommand,SetCommand,IfCommand,RandomCommand,InputSetCommand,InputVariableCommandand its subclasses) is immutable now, with dead setters removed (#306, part of #210's "immutable runtime model" cleanup — an investigation pass found most of the hierarchy's setters were already unused, including at parse time, so this landed as a smaller change than #210 originally scoped). Highlights:ActionCommand.setType()/setValue()/setParameters(),SetCommand.setExpression(),IfCommand.setIfClauses()/addIfClause()/setElseClause(),IfCommand.Clause.setExpression()/setStatement(),RandomCommand.setClauses()/addClause(),RandomCommand.Clause.setWeight()/setStatement(),InputTimeCommand.setGranularityMinutes()/setStartTime()/setMinTime()/setMaxTime(),InputSetCommand.setOptions()/Option.setVariableName()/Option.setText(),InputVariableCommand.setVariableName(),InputNumericCommand.setMin()/setMax(), andInputAbstractTextCommand's eleven setters are all removed.IfCommand/RandomCommand/InputSetCommandlose their no-arg constructor in favor of a full constructor (IfCommand(List<Clause>, NodeBody),RandomCommand(List<Clause>),InputSetCommand(List<Option>));InputSetCommand.Optionsimilarly loses its no-arg constructor in favor ofOption(String, VariableString).InputNumericCommand's single-arg constructor is replaced byInputNumericCommand(String, Integer, Integer).InputAbstractTextCommand's eleven optional fields (length bounds, capitalization/character hints) move to a newInputAbstractTextCommand.Builder, populated viaInputAbstractTextCommand.parseAttributes(Builder, ...)(previouslyparseAttributes(InputAbstractTextCommand, ...)) and passed toInputTextCommand/InputLongtextCommand's constructor — replacing their previous single-arg constructor plus eleven individual setter calls.Translatorno longer mutates a clonedIfCommand/RandomCommand's clauses in place during translation — it rebuilds a new one via the constructors above, same as it already did forNodeBody/Reply.
- Breaking: Core:
NodeBodyis now immutable, and execution now produces a separateResolvedNodeBodyinstead of the sameNodeBodytype — the result ofNodeBody.execute()is structurally narrower than a script's ownNodeBody(a<<set>>/<<if>>/<<random>>segment can never legitimately survive execution; only text and resolvedActionCommand/InputCommandsegments can), anddlb-core-javahad no type distinguishing the two (#305, part of #210's "immutable runtime model" cleanup). Highlights:NodeBody/Replyconstruction moves toNodeBody.Builder/Reply.Builder—addSegment(),addReply(),addCommand(),setStatement(),setNodePointer(),setReplyId(),setCommands(), andclearSegments()are all removed from the built types.NodeBody.execute(Map, boolean, NodeBody)(void, writing into a caller-supplied body) is replaced byNodeBody.execute(Map, boolean), returning a newResolvedNodeBody.Command.executeBodyCommand(Map, NodeBody)is nowexecuteBodyCommand(Map, ResolvedNodeBody.Builder)— affects every command subclass (SetCommand,ActionCommand,IfCommand,RandomCommand, theInputCommandfamily).Node.getBody()andReply.getStatement()are now typed against a new sharedNodeContentinterface, since each legitimately holds aNodeBodybefore execution and aResolvedNodeBodyafter — code that already knows which one it has should cast.Nodeitself is immutable now too (setHeader()/setBody()/the no-arg constructor are gone) — a direct consequence ofNodeBody.execute()no longer needing a mutable body to write into.Translatableno longer carries aparentfield — it was used only to splice a translation back into a mutable tree in place, which no longer happens (Translatorrebuilds instead).
- Breaking: Core:
NodeHeaderis now immutable —setTitle(),setSpeaker(),setOptionalTags(), andaddOptionalTag()are removed,getOptionalTags()now returns an unmodifiable view, and the no-arg constructor is gone in favor ofNodeHeader(title),NodeHeader(title, speaker),NodeHeader(title, optionalTags), andNodeHeader(title, speaker, optionalTags)(#304, part of #210's "immutable runtime model" cleanup). Every field is now set at construction and never changes afterward, matching howDialogue's own getters already defensively wrap their results —NodeHeaderwas the one place that claim didn't actually hold. - Core: parsing a node's body (
BodyParser/CommandParser/ReplyParser) now reports every error found within it instead of stopping at the first (#211) — a node with an unrecognized command, a malformed reply, and another bad command further down previously surfaced only the first of the three; all three are now reported together. A node with any error is still excluded from the dialogue exactly as before — this only changes how much gets reported when something's wrong, not whether it counts as broken. - Breaking: Core:
ActionCommand'stypeis now theActionTypeenum (IMAGE/VIDEO/LINK/GENERIC) instead of a plainString— its constructor,getType(), andsetType()all changed signature accordingly, and theTYPE_IMAGE/TYPE_VIDEO/TYPE_LINK/TYPE_GENERIC/VALID_TYPESconstants are removed (useActionType.IMAGEetc., orActionType.fromWireValue(String)). Previously the "must be one of four known values" invariant was enforced only by the staticparse()factory method, via aList<String>.contains()check external to the class — the public constructor andsetType()didn't validate at all, so constructing one directly with an arbitrary string bypassed it entirely. The wire protocol (.dlbscripttype="..."attributes, and thetypefield the web service sends clients) is unaffected — both still use the same lowercase strings as before. - Studio: the Variable Browser's value tooltip now shows a variable's last-updated time in the time zone of the person using Studio (with the zone named), like the new History mode, rather than in the zone it was recorded in (#323).
Fixed
- Web service: fixed a
ClassCastExceptionthat broke every dialogue interaction past the first node.UserService.getDialogueState()reconstructs anActiveDialogueper request (the API is stateless between calls) and restores its current node straight from the raw dialogue definition — script-stage content, not yet executed.ActiveDialogue.getUserStatementFromReplyId()/processReplyAndGetNodePointer()assumed that node was always already-executed content and cast it accordingly, which threw on every restored session. Breaking: fixed by addingfindReplyById(int)to the sharedNodeContentinterface (NodeBody/ResolvedNodeBodyalready implemented it identically; only the interface declaration was missing) — reply IDs are assigned once at parse time and never reassigned, so the lookup is valid against either script-stage or resolved content, removing the need to know which one aNodeis currently holding.
[0.2.1] - 2026-09-23
Added
- Client JS:
@dialoguebranch/client-jsis now published to npm (#230) — a newrelease: published-triggered workflow runs its tests andnpm publishes it on every GitHub release, keeping the published version in lockstep with the monorepo-wide version. Previously"private": true, consumed only via afile:dependency from within this monorepo.
[0.2.0] - 2026-09-23
Changed
- Breaking: Client JS:
DialogueBranchAuthoringClient.listUsersnow takes a single options object ({ usernameFragment, page, pageSize }) instead of a positionalusernameFragmentstring, and exposes the Web Service's existingpage/pageSizepagination on/users— previously always fetched page 0 only (undocumented beyond a JSDoc note), so a deployment with more than 50 known users couldn't reach the rest through Studio's "run as this user" picker.
Fixed
- Client JS:
packages/client-js/package.json's version now stays in sync with the monorepo-wide version inglobal.json.infrastructure/release/release-github.shand its GitHub Actions equivalent (release-prepare.yml) both rannpm run sync-versionforapps/studiobut never forpackages/client-js, even though it has the identical script — itspackage.jsonhad drifted two patch releases stale (0.1.8whileglobal.jsonhad moved on to0.1.10). Harmless while the package stays unpublished, but would have shipped a wrong version to npm once it is. A new packaging test now asserts the two stay in sync.
[0.1.10] - 2026-09-23
Fixed
- Studio: the
apps/studioDocker image builds again. Its Dockerfile didn't copypackages/client-jsinto the build context, so thefile:dependency on@dialoguebranch/client-jscouldn't be resolved andvite buildfailed.
[0.1.9] - 2026-09-23
Fixed
- BFF:
apps/bff's/api/**proxy no longer double-encodes the forwarded query string.ApiProxyControllerforwards the incoming request's raw, already-percent-encoded query string verbatim, butRestClient's default URI-building encoded it a second time — an already-encoded value liketimeZone=Europe%2FLisbonhad its literal%re-encoded to%25, corrupting it intoEurope%252FLisbonby the time it reached the Web Service, which then rejected it as invalid. Affected any Studio request whose query string needed escaping (a project slug or variable name/value with a space,&,/, etc.), not justtimeZone— found via real browser testing while verifying #252, sincetimeZonehad never actually been percent-encoded before that change and nothing else routinely triggered the double-encoding. - Client JS:
ConsoleLoggernow sends each log level to its corresponding console method (console.error/warn/info/debug) instead of alwaysconsole.log, so browser/Node console severity filtering and error stack capture work as expected (#247). - Client JS: correct small JSDoc/README inaccuracies (
constrcutortypo,Statement/AbstractLoggerparam types,`/whoami`markup,segmentsgetter wording) and add missing class/constructor docs onStatementandDialogueStep(#248).
Removed
- Breaking: Core: retired the pre-flattening multi-source-language handling in
dlb-core-java(#109). Since a project has exactly one source language, the code path for a dialogue name resolving to several.dlbscripts (one per language folder) was dead except on a malformed project. Removed:execution.parser.DirectoryScriptLoader(its metadata-less "every sub-folder is a language" scan is exactly the retired model — useProjectScriptLoaderwith adlb-project.xml), the unusedexecution.parser.ResourceScriptLoader, andi18n.LanguageFinder(its only callers were the multi-variant source-language fallback).ProjectParser/ExecutableProjectnow do a single by-name lookup for a dialogue's source. (#87): thecom.dialoguebranch.model.editpackage (Editable,EditableBody,EditableHeader,EditableNode,EditableProject,EditableScript,EditableTranslation,EditableTranslationSet), thecom.dialoguebranch.editing.parserpackage (EditableBodyParser,EditableHeaderParser,EditableProjectParser,EditableScriptParser,EditableTranslationParser),editing.writer.EditableScriptWriterandEditableTranslationWriter,editing.warning.ParserWarning, andcli.CommandLineRunner. This was the model layer for a Java-based dialogue-editing UI toolkit that was never built and was never functionally complete; editing now happens in Dialogue Branch Studio against the Web Service's own draft model.editing.writer.ProjectMetaDataWriterandcli.DialogueBranchCLI(the module's actual CLI, now repointed onto the runtime model) are unaffected. Breaking, but the project is pre-1.0 (0.x), so this ships in the next minor version rather than waiting for a major. - Breaking: Removed further stale CLI/vendor tooling from
dlb-core-java(#94):i18n.POEditorTools(interactive import/export tooling tied to the POEditor SaaS, unused for years and with no tests), andexecution.parser.DialogueBranchParser'smain()/showUsage()entry point (a single-file parse CLI made redundant bycli.DialogueBranchCLI;DialogueBranchParseritself is unaffected). Also removedi18n.TranslationFile.writeToTSVFile(), orphaned by #87's removal of its only caller. Ships in the next minor version, alongside #87 (pre-1.0, see above). - Breaking: Removed further dead code from
dlb-core-java, found in a follow-up scan after #87/#94:exception.ScriptParseException,exception.FileSystemException, andexception.VariableException(each used only by the deleted editable-model layer),model.common.ScriptTreeNode(used only by the deleted editable-model layer and the CLI code already repointed off it in #87),i18n.TranslationTerm(used only by the deletedPOEditorTools), and two pre-existing orphans unrelated to either prior removal:model.common.DatabaseStorageSource(aStorageSourceimplementation never instantiated anywhere — the Web Service's DB-backed draft storage uses its own separate entities instead) andmodel.execute.DialogueStatus(an unused enum, never wired intoDialogueState). Ships in the next minor version, alongside #87/#94 (pre-1.0, see above).
Added
- BFF:
apps/bffnow exposes a genericGET /loginend-point (LoginController) (#261). Studio's login trigger used to navigate straight to/oauth2/authorization/keycloak, this service's own fixed registration id, an internal detail a differently-structured BFF (one resolving a registration per request rather than having exactly one fixed client) has no way to predict.apps/studio/src/auth.jsnow navigates to/logininstead, and any BFF fronting Studio can satisfy that same generic path however its own registration scheme works. Purely additive:/oauth2/authorization/keycloakitself is unchanged and still works. - New
packages/client-js: a JavaScript client for a Dialogue Branch Web Service,@dialoguebranch/client-js(#88), extracted from what used to be Dialogue Branch Studio's in-app client library. Two classes:DialogueBranchClient(the default export) for running dialogues against a Web Service, andDialogueBranchAuthoringClient(./authoring) for project/dialogue authoring (#231) — a playback-only consumer (e.g. a future public demonstrator front end) only ever needs the former. No framework dependency, so it can be used outside Studio. Published as plain ESM with no build step. Not yet published to npm; Dialogue Branch Studio consumes it locally for now via afile:dependency. Comes with aREADME.md(install, a playback quickstart, the full list ofexportssubpaths) and full JSDoc on both classes' public methods (#233). - Web Service: an External Variable Service (EVS) may now report which variables it supports for a project, via an optional
GET /v{version}/variables/supported?projectSlug=<slug>end-point on the EVS (#185). The Web Service exposes this to clients asGET /variables/list-supported?projectSlug=<slug>(editor/adminrole, same as/variables/list-project), proxying each call to the EVS live with no caching. This is purely a discovery mechanism for authoring tools (e.g. confirming a$variablereferenced in a dialogue is actually one the configured EVS knows about) — the Web Service does not validate, warn, or change execution behavior based on it, and dialogue execution remains fault-tolerant as before (a variable with no value is not an error). An EVS that does not implement the new end-point is unaffected; there is no API-version bump.apps/mock-variable-serviceimplements the new end-point too, reporting the same three variablesretrieve-updatesalready knows about (currentDate,currentTime,dayPart), regardless of the requestedprojectSlug. Dialogue Branch Studio's Variable Browser "Used" view now merges this into the existing list of variables referenced by the project's dialogues: a variable the configured EVS supports but no dialogue references yet still gets a row, and any row the EVS reports gets a small badge. Best-effort — a deployment with no EVS configured (the common case) sees no change from today's behavior. - Core:
dlb-core-java's public API now carries a machine-readable nullness contract (#95). Every package is JSpecify@NullMarked(via a newpackage-info.javaper package, which also adds the previously-missing package-level Javadoc), so unannotated reference types are non-null by default and the genuinely-nullable getters, setters, fields and parameters are marked@Nullable— e.g.Reply.getStatement(),ProjectMetaData.getSlug(),VariableStore.getVariable(),ProjectParserResult.getProject(). Adds anorg.jspecify:jspecifydependency (annotations only, no runtime cost). Not a binary-compatible break, but downstream builds with strict null analysis may surface new warnings. - Core: the
dlb-core-javanullness contract is now enforced in the build (#117). NullAway runs overcom.dialoguebranch.*on every compile (a compile-only error-prone check, no runtime or published-classpath impact) and a violation fails the build. Getting there refined roughly 150 further nullness spots the #95 Javadoc pass had deferred —@Nullableon genuinely-optional values (Variable.getValue(),AttributesCommand.read*Attr(...),NodeParseException.getNodeTitle(), thei18nspeaker/addressee parameters, …) and explicit guards where a value is contractually present — so the contract is both more complete and machine-checked. Downstream builds with strict null analysis may surface correspondingly more warnings. - Core: the expression engine vendored from
rrd-utilsin #102 (com.dialoguebranch.expression/.expression.types/.io/.util) is now under the same@NullMarked+ NullAway contract as the rest ofdlb-core-java(#121) — it had been excluded from the check. Its genuinely-nullable public surface is now@Nullable:Value.getValue()and theValue(...)constructor,Token.getValue(),Expression.evaluate(...)'svariablesargument,ExpressionParser.readExpression()/readOperand(), andCurrentIterator.getCurrent(). Evaluation behaviour is unchanged. The two intricate state-machine classes (Tokenizer,ExpressionParser) andValue/LineColumnNumberReaderkeep a class-level NullAway suppression for their internal dataflow; documenting the engine's members is still open on #121. - Core:
ProjectParsernow detects orphaned nodes — a node that no reply link (internal or external) points to, and that isn't its own dialogue's Start node (#105). This can never cause a runtime error by design (a dialogue isn't required to link every node it defines), so it's reported as a warning via the existingProjectParserResult.getWarnings(), already visible inDialogueBranchCLI's project summary. - Core:
cli.DialogueBranchCLI(renamed fromProjectTool— see below) now supports non-interactive, scriptable invocation alongside its existing interactive menu (#105):DialogueBranchCLI <path-to-dlb-project.xml> [--validate]parses a project and prints its summary, exiting non-zero on parse errors (warnings, such as orphaned nodes, do not affect the exit status) — usable as a CI gate.DialogueBranchCLI <path> --execute <language> <dialogue>runs a specific dialogue, reusing the same interactive terminal conversation as before. The no-argument interactive session is unchanged and remains the default. - Permission-based access control for the Web Service (#28): a
Permissioncatalogue, aRole→ permission mapping (participant⊂editor⊂admin), and anAuthorizationServicethat decides whether a caller may perform an operation. - Studio:
<<action>>commands now render in the dialogue view (#178). An action attached to an agent statement is shown inline —linkas an anchor,imageas an<img>,videoas a player for a direct media URL (a labelled link otherwise), andgenericas a chip showing its value. Actions attached to a reply are logged to the Debug Console on selection; the participant view also emits adlb-actionCustomEventonwindowfor an embedding page to react to. Previously every non-text statement segment was dropped from the rendered dialogue. - Studio:
<<input>>replies now work in the dialogue view (#177). A reply containing an<<input>>command renders the matching widget inline (single-line / multi-line text, email, number, time picker, or a checkbox group forset), validates it against the command's parameters, and submits the collected value(s) to the Web Service as the/dialogue/progress(and/draft/progress) request body — which previously was never sent, so input replies could not be used at all. Applies to both the authoring test view and the participant view. - Web Service + Studio: pick a delegate user by name instead of typing a raw subject (#130). New
GET /v1/usersendpoint — a read-only, realm-scoped listing of the users this service has run a dialogue for ({ username, subject }entries, optionalusernamesubstring filter, paginated), gated on a new admin-onlyUSER_LISTpermission. Studio's "Set Delegate User" dialog is now a typeahead against that endpoint: the admin types a username, picks from the results, and Studio sends the resolvedsubjectto the dialogue endpoints; a raw-subjectfield remains for users not yet known to the service.
Changed
- Breaking: Client JS:
DialogueBranchAuthoringClient'sstartDraftDialogue,progressDraftDialogue, andrevertDraftVariablesnow take a single options object instead of positional arguments, and no longer silently infertimeZone— it's sent only when explicitly given, the same treatment #252 gaveDialogueBranchClient(#256).BaseClient'sIntl.DateTimeFormat()-computed_timeZonefield is removed entirely — nothing reads it any more, on either client class. - Breaking: Client JS:
DialogueBranchClient'slistDialogues,startDialogue,continueDialogue,getVariables,getOngoingDialogue, andsetVariablenow take a single options object instead of positional arguments, and no longer require (or silently infer)projectSlug/language/timeZone— each is sent only when explicitly given (#252). Previously these were positional andtimeZonewas always auto-computed fromIntl.DateTimeFormat(); a deployment behind a backend that resolves the project/language/time zone itself (from the caller's own account or session) can now omit them entirely rather than having the client send values the backend will ignore. A direct Dialogue Branch Web Service consumer must now passtimeZoneexplicitly — the client never infers it. Also addsDialogueBranchClient.back(loggedDialogueId, loggedInteractionIndex), wrapping the previously-unexposedPOST /dialogue/backendpoint. - Breaking: Client JS: every wire-protocol model type now has a
fromJSONparser (previously onlyAction/Segmentdid, despiteprotocol.jsclaiming otherwise) (#232).getServerInfo()andgetOngoingDialogue()now returnServerInfo/OngoingDialogueinstances instead of raw parsed JSON, matching every other method that already returned a typed model. Also fixed two model classes that had drifted from the Web Service's actual response shape:OngoingDialoguewas missingloggedDialogueIdentirely (and hadnamewhere the wire usesdialogueName) — harmless before since nothing constructed one, but would have silently dropped the field Studio's own "resume dialogue" prompt needs;Variabledidn't formally modelupdatedSourceeven though it's part of the real response and Studio already displays it. Breaking for anyone relying on the old plain-object return shape or the pre-fixOngoingDialoguefields, but nothing outside this monorepo consumes the package yet. - Breaking: Client JS:
DialogueBranchClientandDialogueBranchAuthoringClientnow reject failed requests with aDialogueBranchError(a realErrorsubclass —instanceof Errorand.stacknow work;status/statusText/code/fieldErrors/errorsare still there as properties) instead of a plain object (#234).error.messagekeeps working unchanged. Breaking for anyone destructuring the old plain-object shape, but nothing outside this monorepo consumes it yet. - The bundled
default-testseed project (and the mirroredexamples/project-test) has been reworked into a guided tour that exercises every.dlblanguage feature (#173): new lesson dialogues for statements/escaping,random,action(all four types), andinput(all six types), plus expandedvariables(arithmetic, string coercion) andconditionals(elseif, comparison and boolean operators) lessons, all linked frommenu.dlband fully translated intonl-NLandpt-PT. Project version bumped to2. A newpackages/coreFeatureCoverageTestfails if the project stops demonstrating any construct. - Studio: the Variable Browser "Values" tab now shows only the currently selected project's variables, not a cross-project dump (#86). The client's
getVariables/setVariablecalls pass the project slug; with no project selected the tab is empty. - Breaking: External Variable Service: the
retrieve-updates,notify-updatedandnotify-clearedrequests now identify the user and project withsubject+issuer+projectSluginstead of a single bareuserId(#128 + #86).subjectis the stable OIDCsub,issuerthe realm URL (needed because one instance may trust several realms), andprojectSlugscopes the variables. NoDLB_EXTERNAL_VARIABLE_SERVICE_API_VERSIONbump — the bundledapps/mock-variable-serviceand the docs are updated in lockstep; a real external service must be updated to match. - Breaking: Web Service: Dialogue Branch Variables are now scoped to the project (#86). Projects on one Web Service instance are isolated tenants, so a user who runs dialogues in two projects no longer shares one variable namespace. Variables are keyed
(user, project, name):variablesgains aproject_id(NOT NULL, FK →projects), theUNIQUE(user_id, name)constraint becomesUNIQUE(user_id, project_id, name), and each user's runtimeVariableStoreis now per-project.GET /v1/variables/get,POST /v1/variables/set-singleandPOST /v1/variables/setgain a requiredprojectSlugparameter. Flyway migrationV6adds the column and drops all existingvariablesrows (there is no per-project attribution to recover — dev-only data, no deployment on this schema).packages/coreis unchanged. The External Variable Service request contract and the Studio Variable Browser are updated in the following steps of this change. - Breaking: Web Service: a Dialogue Branch user is now identified by the JWT's
(issuer, subject)pair instead ofpreferred_username(#128).preferred_usernameis not realm-qualified (one instance trusts tokens from multiple Keycloak realms) and is mutable/reusable, so two people could collapse onto one variable store + dialogue history, or have their data orphaned on a rename. Theuserstable gainsissuerandsubjectcolumns (UNIQUE(issuer, subject));usernamebecomes a mutable display attribute, refreshed from the token on each login. ThedelegateUserrequest parameter (/dialogue/*,/variables/*,/log/*) now takes the delegate's OIDCsubjectwithin the caller's realm, not a bare username — there is deliberately no cross-realm delegation. Flyway migrationV5adds the columns and, sincesubwas never stored, wipes the existing user-scoped tables (users,variables,logged_dialogues) — dev-only data, no deployment exists on this schema. The External Variable Service request contract is unchanged in this step; it gainsissuer+subjecttogether with the project slug from #86 in a single following revision. - Core:
execution.parser.ProjectScriptLoadernow validates a project's on-disk layout when it is constructed (#109): the metadata must declare a source language, the source-language folder may hold only.dlbfiles, and each translation-language folder may hold only.jsonfiles (checked recursively; dot-files and non-language entries at the project root are ignored). A violation is aParseException.ProjectParseradditionally reports a dialogue name defined by more than one script file as a parse error, regardless of theScriptLoaderin use. Both were previously accepted silently. - Renamed
cli.ProjectTooltocli.DialogueBranchCLI(#105) — the old name dated back to when it was one of several overlapping CLI entry points (see #87/#94); now that it's the library's sole CLI and does more than inspect projects (it also validates and executes dialogues), the name should say so.build.gradle'smainClassand the jar manifest'sMain-Classare updated to match. - Endpoint authorization is now expressed as one required
Permissionper end-point (resolved from the caller's roles via the central role→permission map) instead of an inline list of accepted roles (#58). Which roles may call each end-point is unchanged, but an authenticated caller who lacks the required permission — including a non-admin who passesdelegateUser— now gets403 Forbidden(previously401 Unauthorized), still with error codeINSUFFICIENT_PRIVILEGES; a missing, expired, or invalid token remains401. - Studio's "you can't do this" tooltips on gated actions (Publish / Configure / Export project, Import / New project) no longer name a specific role — they now read "You don't have permission to …", so they stay correct if the role a capability requires ever changes.
- Core: replaced
rrd-utils'I18nLanguageFinderwith a small nativecom.dialoguebranch.i18n.LanguageFinderbuilt onjava.util.LocaleRFC 4647 lookup, as the first step of dropping thenl.rrd:rrd-utilsdependency fromdlb-core-java(#102). (Subsequently removed again in #109 — its only callers were the now-retired multi-source-language fallback.) - Breaking: Core: vendored the
.dlb<<if>>/<<set>>expression engine intocom.dialoguebranch.expression(+.types), and moved the two parser exceptions it shares with the rest of the library —ParseExceptionandLineNumberParseException— intocom.dialoguebranch.exception(folded underDialogueBranchException), continuing #102's removal of thenl.rrd:rrd-utilsdependency. Evaluation semantics,.dlbsyntax and the wire format are unchanged, but a directdlb-core-javaconsumer that referencednl.rrd.utils.expressions.*types, or caughtnl.rrd.utils.exception.ParseException/LineNumberParseException, must switch to thecom.dialoguebranch.*equivalents. Project metadata (dlb-project.xml) is now read with the JDK's StAX parser instead ofrrd-utils' SAX helper, with DTD / external-entity resolution disabled. - Breaking: Core: dropped the
nl.rrd.utilsxml,io.FileUtilsandjson.JsonObjectusages fromdlb-core-java(continuing #102).editing.writer.ProjectMetaDataWriternow writesdlb-project.xmlwith the JDK's DOM /TransformerAPIs and its entry point changed fromwriteToXMLFile(XMLWriter, ProjectMetaData)towriteToXMLFile(OutputStream, ProjectMetaData)(the caller no longer constructs, or needs, annl.rrd.utils.xml.XMLWriter);model.execute.protocol.NullableResponseno longer extendsnl.rrd.utils.json.JsonObject(itstoString()output changes, and it no longer has map-basedequals/hashCode). Output formatting of the exported metadata file changes slightly (indentation, XML declaration); it still round-trips through the reader unchanged. - Breaking:
dlb-core-javano longer depends onnl.rrd:rrd-utils(#102 — the goal of that issue). The last three uses were vendored or replaced:CurrentIteratormoved tocom.dialoguebranch.util,ReferenceParameterin the body tokenizer became a plainint[]out-parameter, andBodyToken.toString()no longer routes throughDataFormatter.rrd-utilsis dropped from the published POM — a consumer that was relying on getting it transitively fromdlb-core-javamust now declare it directly. - The Web Service (
apps/api) and the External Variable Service (apps/mock-variable-service) no longer depend onnl.rrd:rrd-utilseither (#136) — the platform now has norrd-utilsdependency anywhere.DatabaseExceptionmoves to the existingcom.dialoguebranch.web.service.exception.DatabaseException;DateTimeUtils.nowMs(zone)becomes a two-line local helper overZonedDateTime.now(zone).truncatedTo(MILLIS); the request-body reads useInputStream.readAllBytes; the base-path parse usesjava.net.URI; the forbidden-query-param check parses the query string directly; and theHttpError/HttpFieldError/DialogueListPayloadDTOs dropextends JsonObjectfor a plaintoString(). No REST payload, protocol, or behaviour change. - Core:
execution.parser.ParserResultandProjectParserResultare now constructed with their required value (the parsedDialogue/ theScriptLoader) instead of a no-arg constructor plus a setter, andExpressionCommand's internalReadContentResult/ParseContentResultlikewise (#121) — the fields are nowfinaland the@SuppressWarnings("NullAway.Init")markers are gone.ParserResult()/ParserResult.setDialogue(...)andProjectParserResult()/setScriptLoader(...)are removed; both types are only ever handed back by the parser, never constructed by callers. - Core:
execution.ActiveDialoguenow takes itsVariableStoreas a third constructor argument (#121) — previously the field was set by a mandatorysetVariableStore(...)call right after construction. The two-argument constructor is removed.setVariableStore(...)is kept for the one case that genuinely re-points the store (re-attaching a persistedDialogueStateto a fresh user session). - Core: internal parser cleanup (#121) —
execution.parser.DialogueBranchParsersets itsdialogueName/readerin a canonical private constructor (the public constructors are unchanged), andBodyParser.ParseUntilCommandClauseResulttakes itsbodyas a constructor argument. No behaviour change. - Core:
execution.parser.BodyTokenis now built via a constructor (BodyToken(Type, String text, int lineNumber, int colNumber[, Object value])) rather than a no-arg constructor plus setters (#121);setType/setText/setLineNumber/setColNumberare removed and its fields arefinal.getValue()is now@Nullable(it isnullfor the token types that carry no value).ReplyParserthreads its statement / node-pointer / command sections through as method arguments instead of scratch fields. Tokenisation and reply parsing are unchanged. - Breaking: Core:
model.execute.protocol.DialogueActionis now built via an all-args@JsonCreatorconstructor (type,value, optionalparameters) withfinalfields, instead of a no-arg constructor plus setters (#121).DialogueAction(),setType(...),setValue(...)andsetParameters(...)are removed. The JSON wire format —{"type":…,"value":…,"parameters":{…}}— is unchanged and still round-trips (a JSON document with noparametersfield deserializes to an empty map). - Breaking: Core:
model.execute.protocol.DialogueMessagelikewise moves to an all-args@JsonCreatorconstructor withfinalfields (#121); its no-arg constructor, seven setters andaddReply(...)are removed andDialogueMessageFactorybuilds the message in one step. The/dialogue/*response JSON is unchanged. - Breaking: Core: the three
model.execute.protocol.DialogueStatementsegment classes —TextSegment,InputSegment,ActionSegment— are now built via constructors (TextSegment(String)/ActionSegment(DialogueAction)are@JsonCreators;InputSegment's is called by its custom deserializer) withfinalfields (#121); their no-arg constructors andsetText/setInputType/setDescription/setParameters/setActionare removed. The statement / segment JSON — includingInputSegment's flattened parameter form — is unchanged. - Breaking: Core:
model.common.ProjectMetaDatais now built via aProjectMetaData(name, description, version)constructor withfinalname/description/version(#121); the no-arg and five/six-arg constructors andsetName/setDescription/setVersionare removed, andgetStorageSource()is now@Nullable(only metadata read from a store carries one). The XML parser now yields""rather thannullfromgetDescription()for a project whose metadata has no<description>element.
Fixed
- Web Service:
GET /variables/list-supportednow returns 501 when no External Variable Service is configured and 503 when the configured service is unavailable, instead of reporting both conditions as 500 (#243). - Client JS: URL-encode string query parameters used by dialogue playback and draft-test requests so reserved characters do not corrupt the request URL (#246).
- Client JS: limit the npm package to runtime modules, include its MIT licence, and add repository and issue-tracker metadata (#236).
- Web Service: role extraction now honours
dlb.auth.keycloak.client-id(#104). A deployment that set a non-default client id received tokens whose roles live underresource_access.<that-id>.roles, but role extraction always looked under a hardcodeddlb-web-servicekey — so every authenticated user resolved to no roles and got403 INSUFFICIENT_PRIVILEGESon every permission-gated endpoint.azpvalidation already respected the setting; the two now agree. The "user has no roles" warning also names the configured client id and lists theresource_accesskeys the token did carry. - Core: updated the interactive banner and entry-point Javadoc to use "Dialogue Branch CLI" instead of the old "Dialogue Branch Project Tool" name (#110).
- Studio: the "Continue" button in the balloon test view (shown for auto-forward dialogue steps) was near-invisible — a transparent fill with a border and text colour that matched the grey page background. It now has a light fill, an orange border and dark text, so it reads clearly for participants and editors alike (#98).
Security
- The Web Service now checks each token's
azp(client) claim againstdlb.auth.keycloak.trusted-clients(envDLB_AUTH_KEYCLOAK_TRUSTED_CLIENTS), on top of the existing signature/issuer/expiry checks. Unset, it trusts its ownclient-idand the BFF's (dlb-bff), which covers the standard topology (direct API clients / Swagger UI, and Dialogue Branch Studio via the BFF) with no configuration. Set the property to a comma-separated list to trust additional clients in a shared realm, or to a single*to accept any client from a trusted realm (the previous behaviour); an explicit list replaces the default entirely, so it must still namedlb-bffon a BFF-fronted deployment. Tokens from an untrusted client are rejected with401(#67).
[0.1.8] - 2026-09-01
Changed
- Dialogue Browser entries (folders and dialogues alike) now render as grey row blocks, matching the Variable Browser, and each block is inset by its nesting depth.
- The web client is now called "Dialogue Branch Studio" in its own UI: the footer info bar, the browser tab title, the project-selector subtitle, and the version-mismatch page.
- Studio's balloon test view steps the speech-bubble font size down for long statements (to
text-basepast ~400 characters,text-smpast ~900) so more of a long node fits before the reader has to scroll the page. Within one dialogue run the size only ever shrinks — a shorter node after a long one keeps the reduced size instead of bouncing back up. - Breaking:
POST /v1/project/create-projectnow expects the source-language fields assourceLanguageCode/sourceLanguageNameinstead ofdefaultLanguageCode/defaultLanguageName. The old names are no longer accepted (a request using them fails with "Field 'sourceLanguageCode' is required."). This aligns the create request with the rest of the API, which already usessourceLanguage*everywhere else (the create response,GET /v1/project/get-project, and the database). The bundled Dialogue Branch Studio is updated to match.
Added
- Core:
Reply.isAutoForward()— names the "reply without a statement" check; the conceptual call sites in the parser, executor, message factory, translatable extractor and CLI now use it instead of open-codinggetStatement() == null. apps/studionow has a unit-test setup — Vitest + Vue Test Utils + jsdom, run withnpm test. Initial coverage is the dialogue-testing views (reply-option highlighting, the per-tab scroll API, statement paragraph rendering, and the speech-bubble font-size ratchet) and the Dialogue Browser sort/filter helpers.- The Variable Browser has a Values / Used toggle. Values is unchanged — the logged-in user's stored variable values. Used is a new read-only reference list of every variable name the project's dialogues reference, so you can look up the exact name of a variable (
$userNamevs$user_name…) without having triggered it first; hover a row to copy its name. Names the user already has a value for show solid; ones with no value yet are slightly dimmed (#64). - New end-point
GET /v1/variables/list-project?projectSlug=…(editor/admin) backing the above: returns every variable any of a project's dialogues reads or writes, sorted by name and flagged read/written, regardless of whether a value is stored for it. - The Dialogue Browser has a filter + sort strip below its header. The filter box narrows the tree to matching dialogues/folders (folders auto-expand while filtering, without disturbing the saved expand state). The sort control offers Default (folders first, then alphabetical — the API's own order) and Name (A–Z / Z–A) (a flat sort with folders and dialogues intermixed) in both modes, plus Last updated and Size (by node count, folders kept grouped first) in Authoring Mode.
- The Variable Browser has the same filter + sort strip: a name filter and Name (A–Z / Z–A) ordering (variables now default to alphabetical order rather than the order the service returns them in).
- The authoring
list-dialoguesresponse now includes each draft dialogue'supdatedAtandnodeCount, and is ordered folders-first, so a client can offer sort-by-last-updated and sort-by-size in Authoring Mode. Node counts come from a single grouped query (no node content is loaded). - The debug bar at the bottom of Studio's dialogue test views now also shows the node the current step is in, next to the ephemeral session / logged-dialogue id.
- Studio's admin-only "Technical Information" dialog now also shows the Web Service's version and build time, its configured base URL (
dlb.base-url), the host/port/scheme it observed on the incoming request (as seen by the service itself — behind the BFF proxy this is the address the proxy connected to, not a browser-facing URL), and the Keycloak base URL and realm it validates tokens against. These come from new fields on theGET /v1/info/technicalresponse, which remains restricted to theadminrole.
Fixed
- Parsing a project's metadata (
dlb-project.xml) now rejects alanguage-mapwhose source or translation languages are missing acode, or where two languages share one (case-insensitive), instead of silently building a broken language configuration. list-dialoguesnow returns dialogue names ordered "folders first" — at each/-separated level, sub-folders come before loose dialogues, then alphabetical (case-insensitive) — instead of a plain lexicographic sort. Studio builds its own tree so its Dialogue Browser was already correct; this fixes the order seen by clients that render the API response directly.- Text-mode dialogue testing in Studio no longer shows a fresh dialogue's reply options greyed out (or with one option falsely highlighted as already chosen). The component tracked past reply selections by step index without clearing them when the dialogue was restarted, reloaded, or when switching to another test tab, so stale entries carried over onto the new content.
- Text-mode test tabs now keep their own scroll position. The history view's scroll container and spacer height were shared by every tab, so switching tabs left the view at whatever offset the previous tab had. Each tab's offset is now saved on the way out and restored on the way back in (a tab shown for the first time still pins to the latest statement).
- Dialogue statement text with paragraph breaks now renders as separate paragraphs in Studio's balloon and text test views instead of collapsing into one block. Blank lines in a node's body become
<p>breaks and single line breaks become<br>; the newlines were already preserved by the parser and web service, only Studio's HTML rendering dropped them. - Studio's status bar no longer reports a misleading "Connected to
<host>on port<port>" line. Since Studio talks only to its own origin and the Web Service sits behind the BFF proxy, that host and port were Studio's own, not the Web Service's. It now reads simply "Connected to Web Service v<version>." (the version is still the real one, fetched through the proxy), or "Could not connect to the Web Service." on failure. - The "Create Project" wizard's third step now calls the setting the project's "Source Language" rather than "Default Language", and explains that it's the language dialogue content is authored in and that other languages are added later as translations (#76). This matches the wording already used in the project configuration dialog.
[0.1.7] - 2026-08-12
Added
- The visual dialogue editor's node graph now supports right-clicking empty canvas to create a new node at that location, in addition to the existing "Add Node" toolbar button.
- The visual dialogue editor's node body field now has a right-click menu for inserting Dialogue Branch statements (reply options,
<<set>>,<<if>>/<<elseif>>/<<else>>,<<random>>,<<action>>, and all<<input>>types) at the cursor, instead of writing the raw syntax by hand. - The "Service Unavailable" screen's status icons now show the exact URL each reachability check hit, on hover, to make it easier to tell which backend a failure is actually pointing at.
- The Web Service's resource server now trusts more than one Keycloak realm: the configured
dlb.auth.keycloak.realmplus any realm on the same Keycloak instance named<that realm>-<anything>. This supports a hosting platform that provisions one Keycloak realm per client or tenant on top of its own base realm, all sharing this same Dialogue Branch backend and database; previously the JWT filter validated exclusively against the single configured realm, rejecting an otherwise valid token from any other realm outright. The realm name is only ever taken from a token's own (unverified)issclaim to select which known-trusted realm to check the token's signature against, not to pick an arbitrary network address to call out to.
[0.1.6] - 2026-07-29
Added
- Added a
clientDocker Compose profile (infrastructure/docker/compose.yml) that containerizes the web client and BFF behind a new nginx reverse proxy (infrastructure/docker/nginx/client.conf), mirroring a real deployment (#81). This gives a third local development setup, alongside the existing two:docker compose --profile client up -druns everything (API, web client, BFF, proxy) in Docker on a single origin,http://localhost:8080, with no separate dev server needed. - The BFF (
apps/bff) now persists sessions to MariaDB via Spring Session, instead of the JVM's own heap, so a redeploy no longer silently logs out every signed-in user. This includes theOAuth2AuthorizedClientholding the session's access/refresh token, which Spring Boot's default wiring keeps separately from the session itself and would otherwise still be lost on redeploy. The session inactivity timeout (also driving theSESSIONcookie'sMax-Age) is now configurable viadlb.bff.session-timeout(DLB_BFF_SESSION_TIMEOUT), defaulting to 7 days instead of Spring Boot's own 30-minute default. The cookie is re-issued on every request so it slides forward with activity instead of hard-expiring on a fixed clock from login. infrastructure/keycloak/sync-realm.pynow also reconciles redirect/post-logout URIs on Keycloak clients that already exist, not just clients that are entirely missing. Previously, a change like theclientprofile's new origin never reached a developer's already-provisioned local Keycloak without manually resetting the MariaDB volume.
Fixed
- Fixed BFF logout failing with Keycloak's "Invalid redirect uri" for same-origin deployments (the new
clientprofile, and production behind a reverse proxy). The BFF sentpost_logout_redirect_urito Keycloak as a literal relative/, which Keycloak'send_session_endpointrejects since it requires an absolute, registered URI. This differs from the login-success redirect, which a browser resolves relative to its own current origin regardless. Now resolves viaOidcClientInitiatedLogoutSuccessHandler's{baseUrl}placeholder when the redirect URL is left at its default, honoring the reverse proxy's forwarded headers.
[0.1.5] - 2026-07-29
Added
- Added a Backend-for-Frontend (BFF) service (
apps/bff) between the web client and Keycloak/the Dialogue Branch Web Service (#79). The web client no longer talks to Keycloak or the API directly, and no longer holds an access token in the browser at all — the BFF performs the login exchange with Keycloak and keeps the session's token server-side, so the browser only ever holds a session cookie. All API calls now go through the BFF at/api/**, and a new/whoamiendpoint returns the current session's username and roles. Logging out now also ends the Keycloak SSO session, not just the local one, so logging back in properly re-prompts for credentials instead of silently resuming the same session. Local development gained a matchingbffDocker Compose service (started via the existingapiprofile) and a one-shotkeycloak-syncservice that adds the BFF's Keycloak client to a realm that already existed before this change. - Added a
variable-serviceDocker Compose profile (infrastructure/docker/compose.yml) that builds and runs the mock external variable service alongside MariaDB and Keycloak, for testingdlb-web-service's external variable service integration locally. - The mock variable service now also resolves a
$dayPartvariable onretrieve-updatesrequests, returning"morning","afternoon","evening", or"night"based on the current hour in the user's time zone, alongside the existing$currentDate/$currentTimehandling. - Added an "Insufficient Privileges" screen to the web client: a user who successfully logs in but lacks the required Dialogue Branch role (
admin,editor, orparticipant) now sees an explanation and a Log Out button, instead of being silently logged out and redirected straight back to the login page with no indication of why.
Changed
- The web client's production build now splits
dlb-lib(the framework-agnostic client library for talking to a Dialogue Branch Web Service, intended to be reusable outside this app) into its own JS chunk instead of bundling it into the main app chunk, via amanualChunksrule invite.config.js. - The web client now imports only the specific Font Awesome icons it actually uses, instead of the entire solid and regular icon sets. This shrinks the main production bundle from ~1.49 MB to ~450 kB (minified), removing the build's "chunks larger than 500 kB" warning entirely.
Fixed
- Fixed the mock variable service's Docker image, which failed to build: it referenced source paths that no longer exist and packaged the app as a WAR for a standalone Tomcat instead of the Spring Boot jar it's built as today. Its logging config also referenced a missing resource file, which crashed the service on startup.
- Fixed a
NullPointerExceptionthrown when starting a dialogue that references a variable with no existing value while an external variable service is enabled.
[0.1.4] - 2026-07-22
Added
- Added project export/import: a project's published content can be downloaded as a
.ziparchive ("Export Project" in the project menu, Live Mode only, once published) and re-created as a new project from that archive ("Import Project" from the project selector). New endpointsGET /project/export-projectandPOST /project/import-project. Import validates the archive (size caps, zip-slip protection, project-content validation, slug-collision check) before creating anything in the database. Export fetches a project's dialogues and translations in two batched queries instead of one query per dialogue. Importing an archive preserves its exported version number instead of always restarting the new project at version 1. - Added a service status check to the web client's startup: before redirecting to Keycloak's login page, it now verifies the DLB API and Keycloak are both reachable. If either is down, it shows a status page with a Retry button instead of sending the user to a broken login page. The same check also compares the API's reported software version against the web client's own build version; a mismatch (e.g. a stale cached bundle after a deploy) shows an "Update Available" page with a Reload button instead of proceeding to login.
Fixed
- The web client footer version had drifted from
global.json(still showing v2.0.1 after the v2.0.3 release) becauseapps/web/package.jsonwas never re-synced.infrastructure/release/release-github.shnow runsnpm run sync-versioninapps/webright after bumpingglobal.json, and includesapps/web/package.json/package-lock.jsonin the release commit, so this can't drift again.
[0.1.3] - 2026-07-20
Added
- Moved project metadata (display name, description, translation languages) into the same draft → publish cycle as dialogue content (#75). Editing a project's metadata used to write directly to the live/published record with no review step; a new "Configure Project" window (renamed from "Edit Metadata", and now only available in Authoring Mode) edits a draft copy instead, with a General tab (display name, description, read-only project slug, and the latest published version's info) and a Languages tab (the source language shown read-only, plus add/rename/remove for translation languages). Nothing is sent to the server until "Save Draft" is pressed, and the whole batch of pending changes — metadata plus any language removals, additions, and renames — is applied atomically through a new
POST /project/update-draftend-point: either all of it succeeds, or none of it does, with structured per-field errors reported back for whichever change conflicted (e.g. two languages ending up with the same code). Removing a translation language warns which draft dialogues currently have content in it before it's removed, and — like dialogue deletion — is reversible (with an undo) until the project is next published. Draft and published translation content also gained real referential integrity:DBDraftTranslation/DBPublishedTranslationnow reference their language via a foreign key into a language registry, rather than an unconstrained string column. Finally, publishing now snapshots a project's display name, description, and translation-language list onto the new, immutable published version — exactly like dialogue content already is — so a past published version retains its own historical metadata and language list instead of sharing one project-wide, mutable "current" record. - Replaced the Antora/AsciiDoc-based Documentation Hub with a new VitePress-based site (
documentation/vitepress/), now live at https://www.dialoguebranch.com/docs/. The vendoredantora-ui-defaultfork (documentation/dlb-ui/) had drifted from the rest of the platform's look (pinned to Node 10, a separate gulp/browserify toolchain to maintain) and contributed 48 open Dependabot alerts, all in its ownpackage-lock.json, none reachable from the shipped product. The new site reuses the Web Client Test Application's brand palette and self-hosted "Roboto Slab" font, and is fronted by a header mirroring the rest of www.dialoguebranch.com (Home / Documentation / About / News & Updates) so the docs, marketing site, and web client read as one product. All content was ported over 1:1.
Fixed
- Fixed "Publish Project" staying disabled in the web client's Authoring Mode when the only unpublished change was to translation content. Saving a translation cell in
TranslationEditor .vuealready marked the owning draft dialogue as changed server-side, but the component had no way to tell the rest of the app a save had happened — unlike the node editor (DialogueEditor .vue), which emitsdialogueSavedafter every edit, promptingDialogueBrowser.vueto refetch the draft dialogue list and recompute whether anything is unpublished.TranslationEditor.vuenow emits the samedialogueSavedevent after each successful translation save.
Removed
- Removed the old Antora/AsciiDoc documentation project (
documentation/antora/, including the vendoreddlb-uiUI fork) from the repo now that the VitePress site above has fully replaced it in production.
[0.1.2] - 2026-07-17
Added
- Added support for cross-dialogue reply links (
[[Reply Text.|otherDialogue.NodeId]]) in draft test sessions (/draft/*) — selecting such a reply used to fail with "This reply points to another dialogue, which isn't supported in draft test mode." (#74).DraftExecutionService .startSessionalready parsed the whole project (every draft dialogue, not just the one under test) precisely so sibling references would resolve, but that parsedExecutableProjectwas a local variable, discarded once the session started, leavingprogressSessionwith no way to actually follow a link when one turned up.DraftTestSessionnow keeps the parsed project (and the project's source language, needed to resolve languages the same way published dialogues do — not reliably available from the parsed project's own metadata, since these dialogues are parsed from in-memory content maps, not an actualdlb-project.xml) for the life of the session;progressSessionresolves the target dialogue against it and switches the session over via a newDraftTestSession.switchToDialogue, mirroring howDialogueExecutoralready handles the same case for published dialogues. No Web Client changes were needed —DialogueWorkspace .vuealready updates a tab's displayed dialogue name from whatever the server's response says on every progress step. - Added an optional
startNodeIdparameter to the API'sPOST /dialogue/startend-point, to start a live (published) dialogue session at a specific node instead of always the default "Start" node — mirroring what/draft/startalready supported for draft testing.UserService.startDialogueSessionalready accepted this; only the controller's@RequestParamplumbing was missing. Used by the Web Client: switching the "Test dialogues in:" language selector while a dialogue is actively running in the current tab now restarts that tab's test at its current node, in the newly selected language, instead of silently leaving it running in whichever language it happened to be started in (#69). - Added a small "x" icon to reply options in the Web Client's Speech Bubble and RPG Text test modes (
BalloonDialogueComponent.vue/TextDialogueComponent.vue) when selecting that reply would end the dialogue, so a tester can see this before choosing it rather than being surprised afterward (#73). This was a pure front-end gap: the API already computed and sent this per-reply (ReplyMessage.endsDialogue, set byDialogueMessageFactorywhenever a reply points directly at the dialogue'sEndnode), and the client'sReply/BasicReply/AutoForwardReplymodels andDialogueBranchClientalready parsed it — it just was never rendered anywhere. - Added a dedicated Web Client layout for
participant-only users (noeditor/admin), so theparticipantrole can actually be tested end-to-end. Previouslymain.js's role gate excluded anyone withouteditor/adminentirely, logging them straight back out — and even if let in, a participant can't calllistProjects/getProject/listDialogues(all editor/admin-only), soProjectSelectorPage/MainPagewould just fail to load.App.vuenow routes a participant-only user (checked ahead of the normal routing, so an editor/admin who also carriesparticipantstill gets the full app) to a newParticipantPage.vue, which auto-starts the configured project/dialogue (config.participant.projectSlug/dialogueName, defaulting todefault-test/menu; override viaVITE_DLB_PARTICIPANT_PROJECT_SLUG/VITE_DLB_PARTICIPANT_DIALOGUE_NAME) through the existingBalloonDialogueComponent, using only endpoints a plainparticipantcan actually call (/dialogue/start,/dialogue/progress). If the project or dialogue doesn't exist, the resulting 404 is shown inline (viaBalloonDialogueComponent's existingstartError/"Try Again" state) rather than as a toast, since there's nothing else on this single-purpose page for the toast to sit alongside. - Added an admin-only
GET /info/technicalend-point (TechnicalInfoPayload) returning technical information about the running service — currently the number of active (in-memory)UserServiceinstances (ApplicationManager.getActiveUserServiceCount()). Unlike/info/all, this is not inSecurityConfig'spermitAll()list, so it requires normal bearer-token auth plus theadminrole. In the Web Client, admins now see a small "i" icon after the connection status line in the footer; clicking it opens aTechnicalInfoModalshowing this information (DialogueBranchClient.getTechnicalInfo()). - Added an idle-timeout eviction sweep for in-memory
UserServices. AUserServicewas otherwise only ever removed by an explicit/auth/logoutcall, so a client that disconnects without logging out (closed tab, killed app, expired token, dropped connection) left itsUserService— and itsVariableStore,LoggedDialogueStore— in memory for the remaining lifetime of the server process; sinceactiveUserServicesonly grows and never shrinks on its own, a long-running server accumulates one per distinct user who has ever authenticated, not per user currently active.UserServicenow records alastActivityTime, refreshed on every request that resolves an existingUserService(ApplicationManager.getActiveUserService); a newUserServiceExpirationService(@Scheduled, every 5 minutes) evicts anyUserServiceidle longer thandlb.session.idle-timeout-minutes(DLB_SESSION_IDLE_TIMEOUT_MINUTES, default 60).ApplicationManager.activeUserServicesmoved fromArrayListtoCopyOnWriteArrayList, since it's now mutated concurrently by request threads and the scheduled sweep, not just by request threads racing each other.
Changed
- Gave the Web Client's auto-generated "Continue" reply buttons (
AutoForwardReply— an autoforward with no[[Reply Text.|NextNode]]of its own) a distinct, secondary/outline style in both the Speech Bubble and RPG Text test modes, instead of the same solid orange button used for real, author-defined reply options (BasicReply) (#68). A tester can now tell at a glance which buttons represent an actual choice versus a mechanical "just move on." - Breaking: Renamed the
clientKeycloak role toparticipant, to avoid confusion with the unrelated Keycloak concept of a client (an OAuth2 application registration, e.g. thedlb-web-serviceclient itself) —clientwas never meant to denote "a service acting as a client," only "a basic user who can execute dialogues," whichparticipantstates unambiguously.AuthenticationInfo.USER_ROLE_CLIENTis nowUSER_ROLE_PARTICIPANT(value"participant"), and every end-point role check, the dev-stack'sdialoguebranch-realm.json, and the docs (authentication.adoc,dlb-web/index.adoc, both installation/exploring-the-API tutorials,README.md) were updated to match. This is a clean break, not backwards compatible: any already-provisioned Keycloak realm (local dev instances that already imported the old JSON, staging, production, or any third-party self-hosted deployment) has this role under its old name,client, and must be manually renamed in the Keycloak admin console — the dev-stack's--import-realmonly seeds a fresh realm, so this JSON change does not retroactively fix already-provisioned Keycloak instances. Existingclient-role users will get401 INSUFFICIENT_PRIVILEGESfrom every endpoint until their role is renamed.
Fixed
- Fixed the Web Client's Dialogue Browser not alphabetically ordering dialogues in Authoring Mode (#72). Both sort comparators (
DialogueBrowser.vue's top-level tree andDialogueTreeNode.vue's recursive children) only ever partitioned folders before files — same-category entries always compared as0(equal), so any apparent alphabetical order was purely incidental, preserved from whatever order the underlying API happened to return. That's a coincidence Live Mode's/dialogue/list-dialoguesresponse order usually matched, but Draft/Authoring Mode's/authoring/list-dialoguesdoesn't. Both comparators now break ties withlocaleCompareon the folder/file name, so ordering no longer depends on the API's response order at all. - Fixed the Web Client's
IconButton.vuedisabledprop only changing the button's cursor styling instead of actually disabling it — the native<button>element was never bound to:disabled, so a fast double-click could still fire@clickon a "disabled-looking" button (e.g. mid-request, on the "Refresh current dialogue step"/"Refresh dialogue list" buttons, or the Dialogue Editor's "Cancel dialogue"/"Revert variables"/"Add Node" buttons). - Fixed the Web Client's Dialogue Browser collapsing every expanded folder on every refresh, even when the refreshed list is identical to what's already shown — the common case (#71).
listDialogues()now snapshots the displayed entries (name,isPublished/isNew/isChanged/isDeleted, order-independent) and only resetsopenFolderswhen a refresh's result actually differs from that snapshot — triggered by manual refresh, publish, and creating/renaming/deleting a draft dialogue, not just the initial load. Since a no-op refresh no longer has any visible effect on its own, the "Refresh dialogue list" button now spins while the request is in flight (with the same minimum-visible-duration treatment as the "Refresh current dialogue step" button), so clicking it still gives feedback that it did something. - Fixed the Web Client's "restart dialogue" button (shown once a live/published dialogue test has finished) opening a new tab instead of restarting in the current one (#70).
DialogueWorkspace.vue'sloadDraftDialoguealready accepted an existingtabto restart in place — used byrestartActiveTabfor draft tests — butloadDialogue(the live-dialogue path) had no equivalent parameter, sogetOrCreateEmptyTab()always ran, and since the just-finished tab still has adialogueNameset, it never counted as "empty" and a new tab was created every time.loadDialoguenow accepts the same{ tab, language }optionloadDraftDialoguedoes. - Fixed a project being parsed and loaded into memory twice on a fresh/empty database boot, e.g.
Loading Dialogue Branch project 'default-test' into memory./Successfully loaded...logged twice in a row before the seed-completion message.ProjectSeedServiceseeds a new project by publishing it viaPublishService.publish(), which itself callsProjectLoaderService.loadProjectso a live publish takes effect immediately — butProjectLoaderService.loadOnStartup()(@Order(1), runs right after seeding's@Order(0)) then unconditionally reloads every project in the database, redoing the exact same just-finished load.loadProjectnow skips reloading a project whose exact version is already inApplicationManager, loggingProject 'X' version N is already loaded — skipping.instead. Harmless before (the second load was a correct, idempotent replace) but wasteful and confusing in the logs; only visible on a first-ever/empty-database boot. - Fixed the API's Swagger UI showing "1" (the latest API protocol version) as the document version instead of the actual software version (e.g.
2.0.1).OpenApiSwaggerConfignow reads the version fromDlbProperties.getVersion()instead ofServiceContext.getCurrentVersion(). - Fixed inconsistent "DialogueBranch" branding (missing space) throughout the platform — Swagger UI strings, CLI prompts and error messages (
ProjectTool/CommandLineRunner,POEditorTools), log messages, exception messages, source comments and license headers,web.xml, and documentation prose — now consistently read "Dialogue Branch". Class/package/method names (e.g.DialogueBranchParser,com.dialoguebranch) were intentionally left unchanged, as were a couple of literal"DialogueBranch"strings inexamples/project-testtranslation fixtures that are test content tied to an example dialogue script, not branding. - Removed
apps/api/src/main/webapp/WEB-INF/web.xml, a leftover Java EE deployment descriptor from before the service was rebuilt on Spring Boot's embedded server.apps/api/build.gradledoesn't apply thewarplugin, sosrc/main/webappwas never packaged by the build or read at runtime; the root-path redirect to Swagger UI it once implied is already handled bySwaggerControllerandstatic/index.html. - Fixed
DialogueExecutorresolving cross-dialogue reply links ([[link|OtherDialogue.Node]]) by searching all loaded projects for a matching dialogue name/language, instead of scoping the search to the originating dialogue's own project — even thoughExternalNodePointer(the model class backing these links) only ever resolves relative paths bounded at the project root, so a cross-project reference was never a real possibility to begin with. If two projects happened to declare a same-named dialogue in the same language, a link could silently resolve into the wrong project's dialogue.DialogueExecutornow calls the project-scopedgetDialogueDescriptionFromProject/getDialogueDefinitionForProject(passing the originatingserverLoggedDialogue's project slug) instead of the unscoped, all-projects variants. This also let a whole cluster of now-unreachable code be deleted:UserService'sgetAvailableDialogues/getDialogueDescriptionFromId/getDialogueDefinition(ResourcePointer), andApplicationManager'sgetDialogueDescriptions/getAvailableDialogues()/getAvailableDialogues(String)/getDialogueDefinition(ResourcePointer, TranslationContext).
[0.1.1] - 2026-07-16
Added
- Added an optional
delegateUserparameter to the API's/draft/*end-points (start,progress,cancel,revert-variables), matching the existing/dialogue/*and/variables/*end-points. This lets an admin test-run a draft dialogue on behalf of another user instead of only their own account. - Added a global Live Mode / Authoring Mode toggle to the Web Client header (to the left of the Project menu), replacing the previous behavior where the Dialogue Browser always showed a merged list of published and draft dialogues and the node-graph editor could be opened from any tab regardless of context. In Live Mode (the default), the Dialogue Browser lists only published dialogues, testing always runs against the published
/dialogue/*end-points, and the node editor is unavailable. In Authoring Mode, the Dialogue Browser lists only draft dialogues — with their New/Changed/Deleted status and rename/restore controls — testing runs against the ephemeral/draft/*end-points, and the node editor becomes available. "Publish Project" is now only actionable in Authoring Mode. Switching modes clears all open dialogue tabs. - Added small icons in front of the "Mode", "Project", and "User" labels in the Web Client header menus, for easier visual scanning.
- Added a mouseover tooltip ("Refresh dialogue list") to the Dialogue Browser's refresh button.
- Added a "Test dialogues in:" label before the language selection dropdown in the Dialogue Workspace toolbar, vertically centered alongside the other toolbar controls.
- Added a fourth Translate mode to the Dialogue Workspace's mode switcher (alongside Speech Bubble Test, RPG Text Test, and Edit), available only in Authoring Mode. It lists every translatable term in the active tab's dialogue, grouped by speaker, with an editable cell per translation language and per-cell autosave (mirroring the Variable Browser's dirty-tracking pattern) — no external export/import round-trip needed to edit translations anymore. Since showing every project translation language as its own column stops scaling once there are more than a couple, the user instead picks which one or two languages to display via "Column 2" / "Column 3" dropdowns (Column 3 can be hidden to save space). Backed by two new read-only API end-points,
/authoring/get-translationand/authoring/list-translatable-terms.
Fixed
- Fixed two literal NUL bytes accidentally embedded in
DraftDialogueService.java(in place of plain space characters used as a lookup-key delimiter during dialogue rename). They rendered as ordinary spaces in editors and didn't affect runtime behavior, but caused the file to be misdetected as binary by tools likegrep, silently breaking plain-text search across it. - Fixed an issue in the Web Client where the delegate user selected via the admin's delegate-user picker was not sent to the API's
/draft/*end-points, so testing a draft dialogue while impersonating another user silently ran against the admin's own account instead. - Fixed an issue in the Web Client, where testing dialogues would always use the
/draft/*end-points, even when the project had no unpublished changes. Now, the Interaction Testers correctly use the default/dialogue/*end-points for testing dialogues in projects that are fully published. If any single change is made to any dialogue, the interaction testers switch to "draft test mode", to make sure those new changes are taken into account. This means that if a change is made in Dialogue A, and the user starts a test on Dialogue B, this test will still run using the/draft/*end-points, as this dialogue could refer to new contents in Dialogue A. - Fixed an issue in the API where
/dialogue/continue(and/dialogue/progress) could incorrectly respond with "Dialogue not found" for a dialogue that does in fact exist. The lookup used to resolve an ongoing dialogue was reading from a per-user cache that is only built once when a user's session starts, so any dialogue published after that point was invisible to it. This lookup now resolves dialogues live, scoped to the ongoing dialogue's own project, matching how/dialogue/startalready worked. - Fixed an issue in the API where
/dialogue/get-ongoingand/dialogue/continuecould offer to resume a dialogue whose project had since been republished with different content, even though the dialogue itself still existed under the same name. Each logged dialogue is now pinned to the published version of the project it was started against; if that version is no longer the project's current one, it's treated as stale and no longer offered as resumable. - Fixed an issue in the API where requesting a dialogue in a specific translation language (e.g.
nl-NL) via/dialogue/startcould silently return the source-language content instead, because the language-matching logic never correctly matched against a project's hyphenated language codes and fell through to an unrelated default. Language matching is now an exact match against a project's declared languages, falling back to the source language when there's no exact match — no more silent mismatched-language fallback. - Aligned
/draft/*dialogue testing with the/dialogue/*behavior above: requesting a language a draft dialogue has no translation for now falls back to the source language the same way, instead of rejecting the request outright. - The Web Client will now always switch to the Ephemeral Draft Testing Mode after any dialogue in the project has been edited, even if the change only consisted of moving the position of any node. NOTE: This also means that any "move node" event is immediately saved to the server.
- The "LoggedDialogueId" in the bottom of interaction testers is now correctly cleared when switching to "Ephemeral Test Mode".
- Fixed a term-matching bug in the new Translate mode where a term whose source text spans multiple script lines (e.g. two paragraphs separated by a blank line) always appeared untranslated, even when a matching translation existed. Term keys were built with
Translatable.toExportFriendlyString(), which only trims the ends of the text, while the actual translation-matching engine (Translator) normalizes all internal whitespace (including line breaks) to single spaces before comparing — so a multi-line term's key never matched its stored translation. Extracted that normalization into a new sharedTranslatable.toNormalizedString()(packages/core) and pointed bothTranslatorand the term-listing end-point at it, so term keys are now generated identically everywhere. - Fixed the Debug Console showing double-encoded JSON fields (e.g.
DBDraftTranslation.content, itself a whole JSON document stored as a string) as one long line packed with escaped quotes instead of being indented like the rest of the body.prettyBody()now recursively parses and re-indents any string value that looks like a JSON object or array.
Changed
/draft/*end-point session IDs (draftSessionId) are now generated the same way asloggedDialogueIds: a bare 32-character hex string (dashes stripped from the underlying UUID), instead of the standard hyphenated UUID form. Purely cosmetic — the ID is only ever used as an opaque token by both the API and the Web Client.- Logged dialogues are now stored in the database instead of as JSON files on disk, mirroring how Dialogue Branch Variables are already stored. This also makes looking up a user's most recent ongoing dialogue a single indexed database query instead of a scan of every session file.
- When in the "Balloon Interaction Tester" mode, the avatar image is now carrying some tools when you are testing in "Ephemeral Draft Test" mode, just to make this more visually clear.
- Both the "Balloon Style" and "Text Style" interaction testers show a message "Ephemeral Draft Test"
- Session ID: X in the bottom of the panel to indicate that this mode is active.
- Renamed the "Draft" badge shown in the Dialogue Browser for a dialogue with unpublished changes to "Changed" — now that Authoring Mode only ever lists draft dialogues, labeling one of them "Draft" no longer distinguished it from the rest of the list.
- Renamed the Dialogue Workspace's "Balloon style" and "Text style" mode button tooltips to "Test in Speech Bubble Style" and "Test in RPG Text Style", respectively.
- Squashed the API's ten Flyway migrations (
V1__create_schema.sqlthroughV10__add_draft_dialogue_previous_published_name.sql) into a singleV1__create_schema.sqlreflecting the current end-state schema. Since the API has not yet been deployed anywhere with data worth preserving, there was no reason to keep the intermediate steps (e.g. a table added and later dropped, a column renamed and then renamed again) around as migration history. Any existing local MariaDB volume must be reset (docker compose down -v) before starting the API against this new baseline, since Flyway will otherwise detect a checksum mismatch against the old migration chain.
Removed
- Removed the unused JSON-file-based
VariableStoreJSONStorageHandlerand theVariableStoreStorageHandlerinterface it existed alongside. The interface had only ever had one real implementation (VariableStoreDatabaseStorageHandler, which the API wires up directly by concrete type), so the JSON handler was never instantiated or reachable at runtime; consumers now depend onVariableStoreDatabaseStorageHandlerdirectly instead of the interface. - Removed the unused
DBDraftDialogue.renamedFromfield (and itsrenamed_fromcolumn, dropped viaV9__drop_draft_dialogue_renamed_from.sql). It was meant to let the next publish know which published entry to drop after a rename, but that was never implemented — each publish already writes a full, independent snapshot of the live drafts under their current names, so there was never anything to look it up for. It was faithfully set on rename and cleared on publish, but never read in between.
Security
- Fixed a missing authorization check in the API where any authenticated editor or admin could progress, cancel, or revert another user's
/draft/*test session simply by obtaining itsdraftSessionId, since the session lookup never verified it belonged to the requesting user. Draft test sessions read and write the tester's real Dialogue Branch variables, so this could let one user manipulate another user's variable state without their knowledge./draft/progress,/draft/cancel, and/draft/revert-variablesnow verify the session belongs to the requesting (or, for admins, delegated) user before acting on it.
