Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .claude/skills/fieldworks-winapp/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,10 @@ verifies that launch will succeed.
- `references/research.md`: source-backed rationale for this structure.
- `scripts/Preflight-WinFormsMcp.ps1`: pre-session check (node/npx, package, build, `.mcp.json`).
- `scripts/Set-FieldWorksLegacyMode.ps1`: forces `UIMode=Legacy` — run before EVERY launch.
- `scripts/Resolve-FieldWorksDevRegistry.ps1`: aligns the dev registry (`RootCodeDir`/`RootDataDir`) to
this worktree before launch; auto-realigns when the other worktree is idle, else prints `RESULT=ASK_USER`.
A build running from inside a source tree resolves code and data from that tree and
ignores these values; they govern a build launched from outside one.
- `scripts/Resolve-FieldWorksDevRegistry.ps1`: read-only preview of code/data/ICU overrides
pointing into a source tree. Dev builds use their own `DistFiles`; installed releases can
be redirected by those shared overrides. `Build/Repair-DeveloperRegistry.ps1 -WhatIf`
previews an explicit repair; applying it preserves paths outside source trees.
- `references/headless-rendering.md`: why FieldWorks needs a display-bound desktop; what works/doesn't for
invisible capture (winforms-mcp HEADLESS does NOT render FieldWorks; a Virtual Display Driver was tried
and abandoned — see the doc for why; use visible capture, or RDP for true invisibility). Starts with the
Expand Down
25 changes: 11 additions & 14 deletions .claude/skills/fieldworks-winapp/navigation/launch-or-attach.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,13 @@ returns empty and `winforms_take_screenshot` is blank even though the process is
```
- `Set-FieldWorksLegacyMode.ps1` forces `UIMode=Legacy` in libpalaso's settings store
(`%LOCALAPPDATA%\SIL\SIL FieldWorks\*\user.config`).
- `Resolve-FieldWorksDevRegistry.ps1` ensures `HKCU\SOFTWARE\SIL\FieldWorks\9` `RootCodeDir`/`RootDataDir`
point at THIS worktree's `DistFiles` (a mismatch leaves the main window unbuilt — blank). It auto-realigns
when the other worktree is NOT active (no FieldWorks.exe running from it) and was NOT used in the last 24h
(registry key write time). If it prints `RESULT=ASK_USER`, the other worktree may be active — **ask the
user** before realigning, then re-run with `-Force` if they approve.
Since `FwDirectoryFinder` learned to anchor on the source tree it runs from, an exe built from a current
worktree already reads its own `DistFiles`; keep running the script for older builds, and for the other
registry values (`ProjectsDir`), which are still shared across worktrees.
- `Resolve-FieldWorksDevRegistry.ps1` previews code/data/ICU registry overrides pointing into a
source tree. It is read-only, including with `-Force`. Dev builds use their own `DistFiles`;
these shared overrides can redirect an installed release into incompatible worktree files.
For an explicit repair, preview `Build/Repair-DeveloperRegistry.ps1 -WhatIf`, then run it
without `-WhatIf` after closing installed FieldWorks. It preserves
overrides outside identifiable source trees. It cannot identify a checkout whose solution
file has been deleted or moved.

See the script headers and `../references/mcp-setup.md`.

Expand Down Expand Up @@ -99,12 +98,10 @@ See the script headers and `../references/mcp-setup.md`.
relaunch (Step 0 above). If it is STILL blank in Legacy mode, check whether the
process memory is *flat* (via `tasklist`): flat ≈ loaded-but-stuck (a modal that
is not enumerated, or the main window failed to build), climbing ≈ still loading
(a Debug build + Sena 3 migration can take minutes — keep waiting). Also verify
the dev registry points the running exe at its OWN DistFiles:
`HKCU\SOFTWARE\SIL\FieldWorks\9` `RootCodeDir`/`RootDataDir` should match the
worktree of the launched `Output/Debug/FieldWorks.exe`; a mismatch (a different
worktree's DistFiles) can leave the main window unbuilt. Aligning it is a dev
registry change — confirm with the user before editing.
(a Debug build + Sena 3 migration can take minutes — keep waiting). A current dev build
resolves code and data from its own source tree. For installed releases, run the read-only
registry preflight to check for worktree overrides; use the explicit repair described above.
`ProjectsDir` stays shared with the installed app on purpose; the repair keeps it.

## Exit

Expand Down
Original file line number Diff line number Diff line change
@@ -1,114 +1,19 @@
<#
.SYNOPSIS
Ensure the FieldWorks dev registry (RootCodeDir/RootDataDir) points at THIS worktree before launching,
auto-realigning when it is safe to do so.

Previews source-tree registry overrides that can redirect an installed FieldWorks.
.DESCRIPTION
A FieldWorks dev build resolves its code/config (DistFiles: parts, layouts, configuration) from
HKCU\SOFTWARE\SIL\FieldWorks\9 RootCodeDir/RootDataDir. When those point at a DIFFERENT worktree than
the exe launched, the main window can fail to build (blank window, empty UIA tree), so this
skill aligns them to the running worktree before launch.

Because that registry is shared across all worktrees, this script will NOT clobber it if the other
worktree is actively relying on it. "Actively using it" =
(a) a FieldWorks.exe process is currently running from the other worktree's tree, OR
(b) the FieldWorks registry key was last written within the last 24 hours (FieldWorks writes these
dirs on startup, so a recent write ~= a recent launch from some worktree).
If neither holds, the script realigns the registry to this worktree automatically. If either holds,
it prints `RESULT=ASK_USER` and changes nothing, so the caller can ask the user before realigning.

Dev builds resolve code and data from their own source tree. This preflight is
read-only; use Build/Repair-DeveloperRegistry.ps1 explicitly to remove overrides.
.PARAMETER Force
Realign regardless of the active-use check (use after the user approves).

Accepted for existing callers. The preflight remains read-only.
.OUTPUTS
A final line `RESULT=<ALREADY_ALIGNED|REALIGNED|ASK_USER|NO_REGISTRY>` for the caller to branch on.
RESULT=READ_ONLY after the preview completes.
#>
[CmdletBinding()]
param([switch]$Force)

$ErrorActionPreference = 'Stop'
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..\..')).Path
$thisDist = Join-Path $repoRoot 'DistFiles'
$regPath = 'HKCU:\SOFTWARE\SIL\FieldWorks\9'

function Norm([string]$p) { if ([string]::IsNullOrWhiteSpace($p)) { return '' } try { return ([System.IO.Path]::GetFullPath($p)).TrimEnd('\').ToLowerInvariant() } catch { return $p.TrimEnd('\').ToLowerInvariant() } }

if (-not (Test-Path $regPath)) {
Write-Host "[OK] No FieldWorks dev registry key yet; the exe will set it on first launch." -ForegroundColor Green
Write-Host "RESULT=NO_REGISTRY"; return
}

$props = Get-ItemProperty -Path $regPath
$curCode = [string]$props.RootCodeDir
$curData = [string]$props.RootDataDir
Write-Host "This worktree DistFiles : $thisDist"
Write-Host "Registry RootCodeDir : $curCode"
Write-Host "Registry RootDataDir : $curData"

if ((Norm $curCode) -eq (Norm $thisDist) -and (Norm $curData) -eq (Norm $thisDist)) {
Write-Host "[OK] Registry already points at this worktree." -ForegroundColor Green
Write-Host "RESULT=ALREADY_ALIGNED"; return
}

function Set-DevDirs([string]$dist) {
Set-ItemProperty -Path $regPath -Name RootCodeDir -Value $dist
Set-ItemProperty -Path $regPath -Name RootDataDir -Value $dist
}

if ($Force) {
Set-DevDirs $thisDist
Write-Host "[OK] (-Force) Realigned RootCodeDir/RootDataDir to this worktree." -ForegroundColor Green
Write-Host "RESULT=REALIGNED"; return
}

# The "other" worktree the registry currently points at (parent of its DistFiles).
$otherRoot = if ($curCode) { (Split-Path $curCode -Parent) } else { '' }

# (a) Active now: a FieldWorks.exe running from the other worktree's tree.
$activeProc = $null
try {
$activeProc = Get-CimInstance Win32_Process -Filter "Name='FieldWorks.exe'" -ErrorAction SilentlyContinue |
Where-Object { $_.ExecutablePath -and $otherRoot -and (Norm $_.ExecutablePath).StartsWith((Norm $otherRoot)) } |
Select-Object -First 1
} catch {}

# (b) Used in the last 24h: the registry key's last-write time (FieldWorks writes these dirs on startup).
$recent = $false
try {
$sig = @'
using System;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
public static class FwRegLW {
[DllImport("advapi32.dll", SetLastError=true)]
public static extern int RegQueryInfoKey(SafeRegistryHandle hKey, IntPtr c, IntPtr cc, IntPtr r,
IntPtr sk, IntPtr msk, IntPtr mc, IntPtr v, IntPtr mvn, IntPtr mvl, IntPtr sd,
out System.Runtime.InteropServices.ComTypes.FILETIME ft);
}
'@
if (-not ('FwRegLW' -as [type])) { Add-Type -TypeDefinition $sig }
$key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('SOFTWARE\SIL\FieldWorks\9')
$ft = New-Object System.Runtime.InteropServices.ComTypes.FILETIME
if ([FwRegLW]::RegQueryInfoKey($key.Handle, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero, [ref]$ft) -eq 0) {
# FILETIME fields are signed Int32; reinterpret their raw bits as unsigned before combining.
$high = [System.BitConverter]::ToUInt32([System.BitConverter]::GetBytes($ft.dwHighDateTime), 0)
$low = [System.BitConverter]::ToUInt32([System.BitConverter]::GetBytes($ft.dwLowDateTime), 0)
$lastWrite = [DateTime]::FromFileTimeUtc((([long]$high) -shl 32) -bor [long]$low)
Write-Host "Registry key last written: $($lastWrite.ToLocalTime())"
$recent = $lastWrite -gt (Get-Date).ToUniversalTime().AddHours(-24)
}
$key.Dispose()
} catch { Write-Host "[WARN] Could not read registry key write time: $_" -ForegroundColor Yellow }

if ($activeProc) {
Write-Host "[HOLD] FieldWorks is running from the other worktree (PID $($activeProc.ProcessId)): $($activeProc.ExecutablePath)" -ForegroundColor Yellow
Write-Host "RESULT=ASK_USER"; return
}
if ($recent) {
Write-Host "[HOLD] The dev registry was used within the last 24h (another worktree may be active)." -ForegroundColor Yellow
Write-Host "RESULT=ASK_USER"; return
}

Set-DevDirs $thisDist
Write-Host "[OK] Other worktree not active and not used in 24h -- realigned the dev registry to this worktree." -ForegroundColor Green
Write-Host "RESULT=REALIGNED"
& (Join-Path $repoRoot 'Build/Repair-DeveloperRegistry.ps1') -WhatIf
Write-Host 'Dev code and data resolve from the running source tree.'
Write-Host 'RESULT=READ_ONLY'
151 changes: 151 additions & 0 deletions Build/Repair-DeveloperRegistry.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<#
.SYNOPSIS
Verifies developer registry repair and launch preflight with isolated registry operations.
#>
$ErrorActionPreference = 'Stop'
$testRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('FwRegistry_' + [guid]::NewGuid())
$repairScript = Join-Path $PSScriptRoot 'Repair-DeveloperRegistry.ps1'
$preflightScript = Join-Path $PSScriptRoot `
'../.claude/skills/fieldworks-winapp/scripts/Resolve-FieldWorksDevRegistry.ps1'
$registryState = @{ Values = @{}; Removed = @(); Checks = 0 }

function Assert-True([bool]$Condition, [string]$Message) {
$registryState.Checks++
if (-not $Condition) { throw $Message }
}

function Test-Path {
param([string]$LiteralPath, [string]$PathType)
if ($LiteralPath.StartsWith('HKCU:')) {
return $registryState.Values.ContainsKey($LiteralPath)
}
Microsoft.PowerShell.Management\Test-Path -LiteralPath $LiteralPath -PathType $PathType
}

function Get-ItemProperty {
param([string]$LiteralPath)
return [pscustomobject]$registryState.Values[$LiteralPath]
}

function Remove-ItemProperty {
param([string]$LiteralPath, [string]$Name)
$registryState.Removed += "$LiteralPath\$Name"
$registryState.Values[$LiteralPath].Remove($Name)
}

function Remove-Item {
param([string]$LiteralPath, [switch]$Recurse, [switch]$Force)
if ($LiteralPath.StartsWith('HKCU:')) {
$registryState.Removed += $LiteralPath
foreach ($key in @($registryState.Values.Keys)) {
if ($key -eq $LiteralPath -or $key.StartsWith($LiteralPath + '\')) { $registryState.Values.Remove($key) }
}
return
}
Microsoft.PowerShell.Management\Remove-Item -LiteralPath $LiteralPath -Recurse:$Recurse -Force:$Force
}

function Set-ItemProperty { throw 'Launch preflight must not write registry values' }
function Set-DevDirs { throw 'Launch preflight must not realign registry values' }

try {
$sourceRoot = Join-Path $testRoot 'source tree'
$distFiles = Join-Path $sourceRoot 'DistFiles'
$icuPath = Join-Path $distFiles 'Icu70/icudt70l'
$installPath = Join-Path $testRoot 'installed/DistFiles'
New-Item -ItemType Directory -Path $icuPath, $installPath -Force | Out-Null
Set-Content -LiteralPath (Join-Path $sourceRoot 'FieldWorks.sln') -Value ''
$companyPath = 'HKCU:\SOFTWARE\SIL'
$fieldWorksPath = Join-Path $companyPath 'FieldWorks\9'
$encPath = Join-Path $companyPath 'SilEncConverters40'
$repoPath = Join-Path $companyPath 'EncodingConverterRepository'
$silfwPath = 'HKCU:\SOFTWARE\Classes\silfw'
$silfwCommandPath = Join-Path $silfwPath 'shell\open\command'
$registryState.Values = @{
$encPath = @{
RootDir = Join-Path $sourceRoot 'Output\Release'
DeveloperPluginDir = Join-Path $sourceRoot 'Output\Release\EC\Plugins'
}
$repoPath = @{ Registry = Join-Path $distFiles 'SIL\Repository\mappingRegistry.xml' }
$silfwCommandPath = @{ '(default)' = '"' + (Join-Path $sourceRoot 'Output\Release\FieldWorks.exe') + '" %1' }
$fieldWorksPath = @{
RootCodeDir = $distFiles + '\'
RootDataDir = $distFiles
ProjectsDir = Join-Path $distFiles 'Projects'
}
$companyPath = @{
Icu70DataDir = $icuPath
Icu99DataDir = $installPath
OtherDataDir = $distFiles
}
}

& $repairScript -WhatIf
Assert-True ($registryState.Removed.Count -eq 0) 'Preview must not remove any values'
Assert-True ($registryState.Values[$fieldWorksPath].Count -eq 3) 'Preview must retain code/data'
& $preflightScript
& $preflightScript -Force
Assert-True ($registryState.Removed.Count -eq 0) 'Preflight must be read-only, including -Force'
& $repairScript -Confirm:$false
Assert-True ($registryState.Removed.Count -eq 7) 'Only source code/data/ICU/converter/silfw overrides are removed'
Assert-True (-not $registryState.Values[$fieldWorksPath].ContainsKey('RootCodeDir')) `
'Source code override must be removed'
Assert-True (-not $registryState.Values[$fieldWorksPath].ContainsKey('RootDataDir')) `
'Source data override must be removed'
Assert-True (-not $registryState.Values[$companyPath].ContainsKey('Icu70DataDir')) `
'Source ICU override must be removed'
Assert-True ($registryState.Values[$fieldWorksPath].ContainsKey('ProjectsDir')) `
'Shared projects override must be kept, even inside a source tree'
Assert-True ($registryState.Values[$companyPath].Icu99DataDir -eq $installPath) `
'Installed ICU override must be preserved'
Assert-True ($registryState.Values[$companyPath].ContainsKey('OtherDataDir')) `
'Unrelated values must be preserved'

Assert-True (-not $registryState.Values.ContainsKey($silfwCommandPath)) `
'Source silfw handler must be removed'
Assert-True (-not $registryState.Values[$encPath].ContainsKey('RootDir')) `
'Source encoding converter root must be removed'
Assert-True (-not $registryState.Values[$repoPath].ContainsKey('Registry')) `
'Source encoding repository must be removed'

# Installed (non-source) values are kept.
$registryState.Values[$encPath] = @{ RootDir = $installPath }
$registryState.Values[$repoPath] = @{ Registry = Join-Path $installPath 'mappingRegistry.xml' }
$registryState.Values[$silfwCommandPath] = @{ '(default)' = '"' + (Join-Path $installPath 'FieldWorks.exe') + '" %1' }
& $repairScript -Confirm:$false
Assert-True ($registryState.Removed.Count -eq 7) 'Installed silfw/encoding values must be kept'
Assert-True ($registryState.Values.ContainsKey($silfwCommandPath)) 'Installed silfw handler kept'
& $repairScript -WhatIf | Out-Null
Assert-True ($registryState.Removed.Count -eq 7) 'Preview must not remove silfw'
$registryState.Values[$silfwCommandPath] = @{ '(default)' = '"' + (Join-Path $sourceRoot 'Output\Release\FieldWorks.exe') + '" %1' }
& $repairScript -WhatIf | Out-Null
Assert-True ($registryState.Values.ContainsKey($silfwCommandPath)) 'Preview must keep a source silfw handler'
$registryState.Values.Remove($silfwCommandPath)

$registryState.Values[$fieldWorksPath].RootCodeDir = $installPath
$registryState.Values[$fieldWorksPath].RootDataDir = 'relative/path'
& $repairScript -Confirm:$false
Assert-True ($registryState.Removed.Count -eq 7) 'Installed and relative overrides must be preserved'
$registryState.Values[$fieldWorksPath].RootDataDir = 42
& $repairScript -Confirm:$false
Assert-True ($registryState.Removed.Count -eq 7) 'Non-string overrides must be preserved'
$registryState.Values[$fieldWorksPath].RootDataDir = 'C:relative'
& $repairScript -Confirm:$false
Assert-True ($registryState.Removed.Count -eq 7) 'Drive-relative overrides must be preserved'
Remove-Item -LiteralPath (Join-Path $sourceRoot 'FieldWorks.sln')
$registryState.Values[$fieldWorksPath].RootCodeDir = $distFiles
& $repairScript -Confirm:$false
Assert-True ($registryState.Removed.Count -eq 7) 'A path without the source marker must be preserved'
$registryState.Values = @{}
& $repairScript -Confirm:$false
& $preflightScript
Assert-True ($registryState.Removed.Count -eq 7) 'Missing registry keys must be harmless'
Write-Host "Developer registry tests: $($registryState.Checks) checks passed."
}
finally {
if ([System.IO.Path]::GetFullPath($testRoot).StartsWith(
[System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath()),
[System.StringComparison]::OrdinalIgnoreCase)) {
Remove-Item -LiteralPath $testRoot -Recurse -Force
}
}
Loading
Loading