Skip to content

LT-22784: Add Tools > Apply Motif changes; remove the AI export - #1162

Open
johnml1135 wants to merge 10 commits into
mainfrom
feature/apply-motif-changes
Open

johnml1135 wants to merge 10 commits into
mainfrom
feature/apply-motif-changes

Conversation

@johnml1135

@johnml1135 johnml1135 commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Linguists can review parser results in Motif, a separate tool for checking a project's analyses. Motif lets them approve or disapprove analyses and add new ones. Until now, the only way to get those decisions back into the FieldWorks project was to close FieldWorks and run Motif against the project.

This adds Tools > Apply Motif changes…. It is available in every area, like Send/Receive. FieldWorks asks for confirmation, saves the project, and releases its lock. It then runs Motif once, the same way it runs FLEx Bridge:

motif apply --all-pending --project "<project>.fwdata" --json

What happens next depends on Motif's result:

Motif's result What FieldWorks does What the linguist sees
Changes applied (exit 0) Reloads the project "Motif changes were applied…", with Motif's short summary
Nothing to apply (exit 0) Relocks, no reload "There are no Motif changes waiting to be applied."
Refused because the changes no longer fit (exit 2) Relocks; nothing changed "Open Motif to review them, then try again."
Motif is busy with this project (exit 3) Relocks A Motif-busy message
Motif could not confirm the result (exit 4), or any unexpected result Reloads, never retries "Open Motif to see what happened before you try again."
Motif runs past the time limit Kills Motif, then reloads "Motif did not finish in time and was stopped…"

If relocking fails, FieldWorks reloads instead, so it never keeps editing a project that Motif may have written to. A wait cursor shows while Motif runs.

Time limit and failures. Motif gets ten minutes, which FW_MOTIF_APPLY_TIMEOUT_SECONDS can change to anything from 60 to 7200 seconds. After that FieldWorks kills Motif, waits a few seconds to confirm it stopped, and reloads. Nothing between the unlock and the relock or reload can throw past them:

  • A failed save stops before Motif starts.
  • A Motif failure counts as an unexpected result.
  • If Motif cannot be stopped, FieldWorks neither relocks nor reopens, so the two programs never write the file at the same time.
  • If the project file is missing afterward, FieldWorks says so and names the backup instead of reopening.
  • If the reopen itself fails, FieldWorks says so instead of crashing.

Motif is found before the project is unlocked.

Finding Motif.

  1. The MOTIF_DIR environment variable, a folder containing motif.exe.
  2. Otherwise CliPath under HKCU\Software\SIL\Motif, the full path to motif.exe. Motif installs per user, and its install hook writes this value and its uninstall removes it. A recorded path whose file is gone counts as not installed.

FieldWorks never searches PATH and writes nothing to the registry. If Motif is not installed, the command is disabled. The lookup isn't cached: FieldWorks checks each time the Tools menu opens and again on click, like Send/Receive checks for FLEx Bridge, so installing or removing Motif shows without a restart. A malformed MOTIF_DIR or registry path counts as not installed.

FieldWorks references nothing of Motif's. It reads the exit code and a few stable JSON fields, ok, applied, summary and code, and ignores anything else, so Motif can extend its output without breaking FieldWorks. There is no Undo for applied changes. The confirmation doesn't promise one.

Code.

  • MotifExecutableResolver and MotifApplyRunner are in FwUtils. Discovery and the process are injected.
  • MotifApplyWorkflow holds the save, run, and reload-or-relock decisions, plus message selection. It returns failures instead of throwing them.
  • MotifListener handles the command and is registered in Main.xml. The steps it shares with FLExBridgeListener (committing pending edits, stopping the parser, reopening the project) are in ProjectLifecycle. Reopening disposes the listener, so the handler captures what it needs first and shows its result on the new window.
  • The strings are in LexEdStrings.resx, with translator comments.

Tests. They need no Motif installation.

  • FwUtilsTests:
    • Discovery: environment variable, registered path, a registered path whose file is gone, a malformed MOTIF_DIR, and not found.
    • Argument quoting, and every exit code and JSON case: malformed, missing, unknown fields, and a single invocation on exit 4.
    • The time-limit setting, and the timed-out and could-not-stop results.
    • Real-process tests: one drains more than 64 KB from both output streams, one kills a process that outlives a one-second limit, and one returns although a child process keeps the output open.
  • LexEdDllTests:
    • The save and unlock order, and reload versus relock for each outcome.
    • A relock that throws, a reload that throws, and both. A save that fails. A Motif run that throws or returns nothing.
    • Time-out, could-not-stop, and a missing project file. The reopened window being returned.
    • Message selection.
    • The menu configuration, including that only MotifListener handles the command.
    • The command's enabled state following Motif being installed and removed while FieldWorks is open.
  • By hand, in a debug FieldWorks on a copy of TestLangProj:
    • Against the Motif CLI built from main, the command reported "no Motif changes waiting", relocked the project, and FieldWorks closed cleanly.
    • Against a stand-in motif.exe that never exits, with FW_MOTIF_APPLY_TIMEOUT_SECONDS=60, Motif was stopped and the project reopened, with the time-out message. No motif.exe was left running.
    • Adding and removing the CliPath registry value while FieldWorks was open enabled and then disabled the command.
    • Not run by hand: an apply that actually changes the project, because that needs approved changes waiting in Motif.

The matching Motif change, which releases apply --all-pending and gives the result an exit code, is in Motif's repository. It will be available before this is used outside development.

Also removes the AI grammar and texts export. A separate commit reverts #1070 (11a2396). It removes the experimental "Export Grammar and Texts for AI Analysis" option behind FLEX_AI_EXPORT, its text picker and instructions file, and the bundled Docs/ai-parser-help guide. #1070 also carried changes unrelated to the export. A later commit puts those back as they are on main:

  • the shared EnvironmentVariables helper, used by WEBONARY_API, the Avalonia opt-in and the interaction-trace flags
  • the CONTEXT.md glossary entries
  • the /.review/ ignore rule
  • the BOM on Main.xml

Next step. A follow-up will have Motif apply to a copy of the project, which FieldWorks then publishes only on success. The original then never leaves FieldWorks' lock, and Cancel becomes safe to offer. That needs a Motif CLI change, and it will be linked here once it's filed.

Jira: LT-22784.

🤖 Generated with Claude Code


This change is Reviewable

@codecov-commenter

codecov-commenter commented Sep 27, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 51.53374% with 237 lines in your changes missing coverage. Please review.
✅ Project coverage is 39.49%. Comparing base (a90754c) to head (63e5290).

Files with missing lines Patch % Lines
Src/LexText/Lexicon/MotifListener.cs 12.21% 103 Missing and 12 partials ⚠️
Src/LexText/Lexicon/ProjectLifecycle.cs 0.00% 39 Missing and 2 partials ⚠️
Src/Common/FwUtils/MotifApplyRunner.cs 77.97% 24 Missing and 13 partials ⚠️
Src/Common/FwUtils/MotifExecutableResolver.cs 51.02% 16 Missing and 8 partials ⚠️
Src/LexText/Lexicon/MotifApplyWorkflow.cs 86.95% 4 Missing and 8 partials ⚠️
Src/LexText/Lexicon/FLExBridgeListener.cs 0.00% 8 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1162      +/-   ##
==========================================
- Coverage   39.49%   39.49%   -0.01%     
==========================================
  Files        1537     1535       -2     
  Lines      354392   354562     +170     
  Branches    41058    41100      +42     
==========================================
+ Hits       139974   140018      +44     
- Misses     185044   185146     +102     
- Partials    29374    29398      +24     
Files with missing lines Coverage Δ
Src/xWorks/ExportDialog.cs 13.89% <ø> (-1.10%) ⬇️
Src/LexText/Lexicon/FLExBridgeListener.cs 4.48% <0.00%> (+0.05%) ⬆️
Src/LexText/Lexicon/MotifApplyWorkflow.cs 86.95% <86.95%> (ø)
Src/Common/FwUtils/MotifExecutableResolver.cs 51.02% <51.02%> (ø)
Src/Common/FwUtils/MotifApplyRunner.cs 77.97% <77.97%> (ø)
Src/LexText/Lexicon/ProjectLifecycle.cs 0.00% <0.00%> (ø)
Src/LexText/Lexicon/MotifListener.cs 12.21% <12.21%> (ø)

... and 6 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

NUnit Tests

    1 files  ± 0      1 suites  ±0   13m 7s ⏱️ + 2m 45s
6 644 tests +49  6 559 ✅ +49  85 💤 ±0  0 ❌ ±0 
6 653 runs  +49  6 568 ✅ +49  85 💤 ±0  0 ❌ ±0 

Results for commit 63e5290. ± Comparison against base commit a90754c.

This pull request removes 22 and adds 71 tests. Note that renamed tests count towards both.
LexTextDllTests.AiAnalysisExportListenerTests ‑ Export_UnloadableGrammar_ThrowsAndWritesNothing
LexTextDllTests.AiAnalysisExportListenerTests ‑ Export_WritesTheGrammarAndMarksTheRequestHandled
LexTextDllTests.GrammarExportLoadLoggerTests ‑ InvalidPhoneme_AddsAMessage_DoesNotThrow
LexTextDllTests.GrammarExportLoadLoggerTests ‑ InvalidStrata_AddsTheReasonToTheMessage
SIL.FieldWorks.Common.FwUtils.AiAnalysisExportRequestTests ‑ Constructor_SetsTextsFolderAndGrammarPath_LeavesHandledFalseAndMessagesEmpty
SIL.FieldWorks.Common.FwUtils.AiAnalysisExportRequestTests ‑ Messages_CanBeAppendedByASubscriber
SIL.FieldWorks.IText.FlexTextExporterTests ‑ ExportTexts_OnAThreadPoolThread_WritesTheFileWithoutThrowing
SIL.FieldWorks.IText.FlexTextExporterTests ‑ ExportTexts_OneText_WritesOneFlexTextFile
SIL.FieldWorks.IText.FlexTextExporterTests ‑ ExportTexts_TextNamedLikeTheGrammarFile_DoesNotOverwriteIt
SIL.FieldWorks.XWorks.ExportDialogAiGateTests ‑ IsAiExportTemplate_FalseForEveryOtherTemplate("<somethingElse />")
…
LexEdDllTests.MotifApplyConfigurationTests ‑ MainDefinesTheGlobalToolsCommandAndItsListener
LexEdDllTests.MotifApplyConfigurationTests ‑ OnlyTheMotifListenerHandlesTheMotifCommand
LexEdDllTests.MotifApplyWorkflowTests ‑ ExecuteCommitsEditsAndUnlocksBeforeOneMotifInvocation
LexEdDllTests.MotifApplyWorkflowTests ‑ ExecuteSelectsMessageForEachOutcome(Ambiguous,8,"",True,9,True,True)
LexEdDllTests.MotifApplyWorkflowTests ‑ ExecuteSelectsMessageForEachOutcome(Applied,0,"{\"ok\":true,\"applied\":true}",True,1,True,True)
LexEdDllTests.MotifApplyWorkflowTests ‑ ExecuteSelectsMessageForEachOutcome(Busy,3,"",False,5,True,True)
LexEdDllTests.MotifApplyWorkflowTests ‑ ExecuteSelectsMessageForEachOutcome(CouldNotStart,-1,"",False,8,False,True)
LexEdDllTests.MotifApplyWorkflowTests ‑ ExecuteSelectsMessageForEachOutcome(NoChanges,0,"{\"ok\":true,\"applied\":false}",False,3,True,True)
LexEdDllTests.MotifApplyWorkflowTests ‑ ExecuteSelectsMessageForEachOutcome(NotInstalled,0,"",False,7,True,False)
LexEdDllTests.MotifApplyWorkflowTests ‑ ExecuteSelectsMessageForEachOutcome(ReconciliationRequired,4,"",True,9,True,True)
…

♻️ This comment has been updated with latest results.

@johnml1135 johnml1135 changed the title Add Tools > Apply Motif changes LT-22784: Add Tools > Apply Motif changes; remove the AI export Sep 30, 2026
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Comment hygiene (advisory)

No comment-style violations in the lines this branch adds since origin/main.

@jasonleenaylor jasonleenaylor left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a positive next step. Two changes inline (a timeout for Motif, and the Webonary switch the revert undid), plus a question and two nits.

This review was assisted by Claude Opus 5.5.

Comment thread Src/Common/FwUtils/MotifApplyRunner.cs Outdated
{
var outputTask = process.StandardOutput.ReadToEndAsync();
var errorTask = process.StandardError.ReadToEndAsync();
process.WaitForExit();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WaitForExit() has no timeout, and this runs on the UI thread (OnApplyMotifChanges runs workflow.Execute() under a WaitCursor) after the project has been unlocked, with Motif's window hidden by CreateNoWindow. If Motif stalls, FLEx sits at "Not Responding" with the project unlocked and nothing the user can close. Send/Receive at least has a visible Bridge window and a watchdog.

SIL.CommandLineProcessing.CommandLineRunner (SIL.Core, which FwUtils already references) takes an encoding and a timeout and reports DidTimeOut; XhtmlDocView.cs:926 uses it under a WaitCursor the same way. On timeout, kill Motif and classify the result as Ambiguous so it reloads. It would also replace the hand-rolled stream draining.

While you're in here, I'd move the command out of FLExBridgeListener. Motif isn't FLEx Bridge; it lives there to reach that listener's private save/unlock/reopen steps (ValidatePendingEditorData, StopParser, GetFullProjectFileName, RefreshCacheWindowAndAll) and its global registration. A MotifListener registered in Main.xml, with those steps pulled into a helper both listeners call, would read better, and it avoids using this listener after the reload has disposed it (the reload lambda sets _parentForm, then ShowMotifApplyMessage runs on the old listener).

Please also make sure nothing between the unlock and the relock-or-reload can throw past them. FindExecutable() runs outside Apply's try, and a quoted MOTIF_DIR makes Path.Combine throw on .NET Framework; a failed relock followed by a failed reload does the same.

@johnml1135 johnml1135 Oct 13, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1f1b25e, c27dbf3 and 63e5290.

  • Timeout. RunProcess waits ten minutes (FW_MOTIF_APPLY_TIMEOUT_SECONDS, 60 to 7200), then kills Motif and waits a few seconds to confirm it exited. The result is TimedOut, which reloads. If Motif can't be stopped, FieldWorks neither relocks nor reopens, so the two programs never write the file at the same time. It also stops waiting for output a child process keeps open.
  • CommandLineRunner. I kept the existing runner. In the SIL.Core version we pin (18.0.0-beta0030), CommandLineRunner also blocks the calling thread. It sets the encoding on stdout only, can miss the last of the output after the process exits, and can't tell us whether the kill worked.
  • Listener. The command is in MotifListener, registered in Main.xml. The steps it shares with FLExBridgeListener (committing edits, stopping the parser, reopening) are in ProjectLifecycle. The handler captures the window, cache and app before it starts, and shows its result on the reopened window.
  • Throw paths. Motif is resolved before the unlock, and a quoted MOTIF_DIR counts as not installed. If a save fails after the lock was released, the project is locked again. If waiting fails after Motif started, FieldWorks stops it before deciding anything. MotifApplyWorkflow now returns a failed save, a Motif failure, a missing project file and a failed reopen as results instead of throwing them. Tests cover each case, and they replace ReloadFailurePropagates.

FieldWorks still waits on the UI thread, as Send/Receive does, but the wait is now bounded. A responsive dialog with Cancel is safe only once Motif applies to a copy and FieldWorks keeps its lock. That needs a Motif CLI change, and it's planned as a follow-up; I'll link it here once it's filed.

FwUtilsTests (including real-process timeout tests) and LexEdDllTests pass. I also ran it by hand in a debug FieldWorks:

  • Against the real Motif CLI, it reported "no Motif changes waiting", relocked, and closed cleanly.
  • Against a stand-in motif.exe that hangs, with a 60-second limit, Motif was stopped after about 70 seconds, the project reopened, and the time-out message showed.

Comment thread Src/xWorks/UploadToWebonaryController.cs Outdated
Comment thread CONTEXT.md Outdated

private bool IsMotifAvailable()
{
if (!_motifAvailable.HasValue)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: since this is cached for the window's lifetime, someone who installs Motif while FieldWorks is open won't see the command enabled until they restart. Probably fine, but it may be worth having the Motif installer tell users to restart FieldWorks.

@johnml1135 johnml1135 Oct 13, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than asking for a restart, I dropped the cache in 66a51c6. XCore asks for this item only when it builds the Tools menu and each time Tools opens, not on idle. Send/Receive already checks for FLEx Bridge on every call the same way. The click checks again before confirming, so an uninstalled Motif can't unlock the project for nothing. CommandFollowsMotifBeingInstalledAndRemovedWhileFieldWorksIsOpen runs one listener through absent, installed and removed. I also checked it in a running FieldWorks: adding and then removing the HKCU\Software\SIL\Motif CliPath value enabled and then disabled the command, without a restart. The cost is one registry read and a file check each time Tools opens, so a MOTIF_DIR on a slow network drive would delay the menu.

Comment thread DistFiles/Language Explorer/Configuration/Main.xml Outdated
johnml1135 and others added 10 commits October 13, 2026 13:34
Co-Authored-By: GPT-6 Luna <noreply@openai.com>
Run Motif at the save boundary and restore the cache according to its result.

Co-Authored-By: GPT-6 Luna <noreply@openai.com>
This reverts commit 11a2396 (#1070). It removes the experimental
"Export Grammar and Texts for AI Analysis" option behind FLEX_AI_EXPORT,
its text picker and instructions file, the bundled Docs/ai-parser-help
guide, and the EnvironmentVariables helper that only this feature used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reverting #1070 restored doc comments wider than the 98-column
comment limit. Rewrap them; the wording is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Motif installs per user, and its install hook records the CLI at
HKCU\Software\SIL\Motif, value CliPath. The resolver read HKLM value
InstallationDir, which no Motif package writes, so an installed Motif
was reported missing unless MOTIF_DIR was set. MOTIF_DIR still wins;
a recorded path whose file is gone counts as not installed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reverting #1070 also undid changes that were not part of the export.
Restore them as they are on main:

- EnvironmentVariables and its tests, shared by the interaction-trace
  flags, the Avalonia opt-in and Webonary. WEBONARY_API=0, false or off
  had turned the JSON API back on.
- UIModeGates and its tests, back on the shared helper.
- CONTEXT.md: the "Texts & Words" area name and the Grammar and Word
  occurrence glossary entries.
- The /.review/ ignore rule and the UTF-8 BOM on Main.xml.

Add a Webonary test that reads WEBONARY_API through UseJsonApi.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Motif is not FLEx Bridge, so its command gets its own listener,
registered in Main.xml. The steps both listeners take around an
external edit -- committing pending edits, stopping the parser, and
reopening the project -- move to ProjectLifecycle.

Reopening disposes the listener that started it, so the handler now
captures what it needs first and shows its result on the reopened
window instead of reusing the old listener.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Motif ran on the UI thread with no time limit while the project was
unlocked, so a stalled Motif froze FieldWorks with nothing to close.

- Wait at most ten minutes (FW_MOTIF_APPLY_TIMEOUT_SECONDS, 60 to
  7200), then kill Motif and reopen the project, since it may have
  changed. If Motif cannot be stopped, neither relock nor reopen.
- Stop waiting for output a child process still holds open.
- Resolve Motif before unlocking. A malformed MOTIF_DIR or registry
  path counts as not installed instead of throwing.
- A save failure stops before Motif starts. A Motif failure is
  Ambiguous. A failed reopen, or a missing project file, is reported
  instead of thrown, and a missing file is not reopened.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The listener cached whether Motif was installed for the window's
lifetime. Installing Motif while FieldWorks was open left the command
disabled until a restart, and uninstalling it left the command enabled.
XCore asks for this item only when it builds or opens the Tools menu,
so check each time, as Send/Receive does for FLEx Bridge.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- If waiting for Motif fails after it started, kill it and confirm it
  exited. If it is still running, neither relock nor reopen.
- Check that the project file exists before relocking as well as
  before reopening, so a missing file is reported either way.
- If saving fails after the lock was released, lock the project again.
- Log the exceptions that become an Ambiguous result.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@johnml1135
johnml1135 force-pushed the feature/apply-motif-changes branch from ae0e20d to 63e5290 Compare October 13, 2026 18:12

@jasonleenaylor jasonleenaylor left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, both required changes are fixed. One question and one nit inline, neither blocking.

using (new WaitCursor())
workflowResult = workflow.Execute();

ShowMotifApplyMessage(workflowResult.ReopenedWindow ?? owner, workflowResult.Message,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking question: when Motif can't be stopped, the project file is missing, or the reopen fails, the project stays unlocked and this shows the message on the original window, which is still live and editable. Until the user closes FieldWorks, an edit can still be written to the .fwdata, and with Motif still running that's the two-writers case again. Would closing the window, or making it read-only, on those outcomes be safer than relying on the message?

<command id="CmdChooseLangProject" label="_Open..." message="ChooseLangProject" icon="OpenProject" shortcut="Ctrl+O"/>
<!-- Send/Receive commands. See "ContextHelp.xml" file for corresponding tooltips. -->
<command id="CmdFLExBridge" label="Send/Receive _Project (with other FLEx users)..." icon="syncSendReceive" message="FLExBridge" />
<command id="CmdApplyMotifChanges" label="_Apply Motif changes..." message="ApplyMotifChanges"/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this sits in the Send/Receive commands block, whose comment points to ContextHelp.xml for tooltips, but it's a Tools menu item and has no tooltip there.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants