Releases: KenM76/combridge
Release list
v0.10.1 — Fix: filter invisible/utility windows from MRU Z-order ranking
Closes `FR_sessionpicker_mru_filter_invisible_windows.md` — a bug diagnosed live against multi-instance SolidWorks.
What was wrong
`SessionPicker.RankByZOrder` matched the first top-level window per PID in global Z-order without filtering, so invisible utility windows (`tooltips_class32`, IME input contexts, addin-host shells, drop-shadow hosts) determined MRU rank. Their position in Z-order shifts based on mouse-hover side-effects on host UI — unrelated to which window the user actually clicked. Result: `--session 1` (combridge's default when no session selector is specified) sometimes attached to the wrong instance even when the user had clicked into a specific one moments earlier.
Live diagnostic evidence (3-instance SW setup): every per-PID first-hit was a `tooltips_class32` — `visible=False`, `titleLen=0`, `WS_EX_TOOLWINDOW` set. Mouse hover lifecycle was determining MRU rank.
Fix
4-line filter in `RankByZOrder` skips windows failing any of:
- `IsWindowVisible == false`
- `GetWindowTextLength == 0` (no caption)
- `WS_EX_TOOLWINDOW` set (hidden from Alt-Tab)
- `WS_EX_NOACTIVATE` set (never receives focus)
The canonical "is this user-focusable?" predicate every Alt-Tab and taskbar has used since Win95.
Scope: who's affected
| Plugin | Affected by the bug? | Notes |
|---|---|---|
| `solidworks` | ✅ Severely | Confirmed by the FR; real multi-process, no shim |
| `excel` / `word` / `powerpoint` | ⚠ Yes when actually multi-process | Office 365's shared-instance shim usually masks it; real multi-instance hit the same bug |
| `outlook` | ✗ No effect | Single-instance MAPI — only one PID to rank |
| Mac plugins (×4) | ✗ Not applicable | Override `FindSessions` with AppleScript |
Verified
Live-tested against the same 4-instance SW setup that surfaced the bug. `list-sessions` returned identical rank ordering across three successive runs after the fix. Pre-fix, ordering flipped between runs based on mouse-hover state.
Cost: ~3 extra Win32 calls per walked window (sub-microsecond each). Total walk time stays well under a millisecond on ~1500-window workstations.
Lesson captured
`C:\personal_rag\claude_code\lesson_20260616_combridge_zorder_mru_visible_filter.md` — generalises the diagnostic technique (PowerShell Z-order walk comparing unfiltered vs filtered first-hit per PID) to any multi-instance focus-tracking puzzle on Win32.
v0.10.0 — three typed SolidWorks diagnostic commands (active-config, list-configs, list-components)
Closes `FR_solidworks_typed_commands.md` (pending 12 days). Brings the SW plugin to surface parity with the Office plugins' typed `info` / `list-accounts` / `search` commands.
Added
`solidworks active-config` — emit `{"path","title","type","config"}` for the active doc as JSON. Empty shape with exit 0 when no doc is open.
`solidworks list-configs []` — emit `{"path","active","configs":[{"name","is_derived"}...]}`. Reuses live doc if open, otherwise silent read-only `OpenDoc6` → read → `CloseDoc`. Active-doc fallback when path omitted.
`solidworks list-components [] [--config ]` — emit `{"path","config","components":[{"name","path","config","suppressed"}...]}`. Walks `IAssemblyDoc.GetComponents(false)` — all components recursively. Optional `--config` walks a specific configuration with defense-in-depth `ShowConfiguration2` + `ForceRebuild3` verification.
Safety discipline baked in
All four cited `personal_rag` lessons are wired into the implementation:
- OpenDoc6 silent-config-arg bug — pass actual config name, never `""` (except for `list-configs` which reads NAMES that are config-independent)
- ForceRebuild3 invalidates COM pointers — rebuild BEFORE component walks
- `as` on COM RCWs returned as object is unreliable — every such cast is hard-cast inside try/catch. `as` is used only on TYPED returns
- CloseDoc leaves components resident — `CloseDoc(title)` only the doc we opened; NEVER `CloseAllDocuments(true)` (would destroy user's unsaved work)
Live-verified
Against running SW 2026 SP1.1 with `TS-0229-180192.SLDASM` open:
- `active-config` → correct active config (`DOORS PARALLEL TO FRAME`)
- `list-configs` → 3 configs (Default + 2 derived) — `is_derived` flag correct
- `list-components` → 769 components walked, 65 suppressed correctly identified (proves we're in the right config — the exact failure mode the OpenDoc6 lesson warned about)
- Active-doc fallback (no path arg) → same result as explicit path
- Error paths → exit 1 with clear messages
See `CHANGELOG.md` v0.10.0 entry for full implementation detail.
v0.9.0 — run-script as a clean Unix filter (ScriptArgs, Stdin, stderr separate)
Closes TWO FRs that compose into one story:
- `FR_runscript_script_args.md` — argv channel via `ScriptArgs` global
- `FR_runscript_stdin_and_stderr_separation.md` — `Stdin` global + stop redirecting `Console.Error` to the output writer
The Args FR had been misfiled to `Complete/` at some point without code actually shipping (verified by repo-wide grep for `ScriptArgs` returning zero matches). v0.9.0 ships both together because they compose: a `.csx` becomes a proper Unix-style stdin → stdout(data) + stderr(diag) filter with argv in and exit code out. The SWBomExcluded ScripTree provider can retire its PowerShell wrapper shim and become plain `combridge solidworks run-script provider.csx -`.
Use it
# ScripTree provider pattern — stdin JSON in, choices JSON out, diagnostics on stderr
echo '{\"target_file\":\"foo.SLDDRW\"}' | combridge solidworks run-script provider.csx --mode quick -Inside the `.csx`:
using System.Text.Json;
Console.Error.WriteLine($\"[diag] received {Stdin.Length} bytes on stdin, {ScriptArgs.Length} args\");
var req = JsonSerializer.Deserialize<ProviderRequest>(Stdin);
// ... build choices ...
Console.WriteLine(JsonSerializer.Serialize(new { choices, choice_labels }));
return 0;Same channels work in `.vbs` — `ScriptArgs(0)` and `Stdin`.
The hang trap caught during smoke-test
Naive `if (Console.IsInputRedirected) ReadToEndAsync()` hangs forever when stdin is inherited-but-empty (combridge invoked from bash subprocesses, Task Scheduler, CI runners). `IsInputRedirected` returns `true` for "non-terminal" — NOT "has data available." Fixed with a cancellation-token timeout pattern. Captured as `personal_rag/claude_code/lesson_20260615_console_isinputredirected_inherited_handle_hang.md` for the next implementer.
Verified
All four smoke tests pass end-to-end:
| # | Test | Result |
|---|---|---|
| 1 | ScriptArgs (no pipe) | 5 args parsed, no hang, 250 ms wall clock |
| 2 | Real stdin pipe | 66 bytes received cleanly |
| 3 | stderr split | STDOUT to ``, STDERR to real stderr |
| 4 | Full filter | clean JSON on stdout, diag on stderr, args + stdin |
See `CHANGELOG.md` v0.9.0 entry for full implementation detail and the IScriptContext interface design.
v0.8.2 — run-script propagates script's return N as exit code (contract fix)
Closes `FR_runscript_propagate_script_return_value.md`. A `.csx` written as `Console.WriteLine("probe"); return 5;` now exits with code 5, not 0.
The documented behavior in `ScriptHost.RunAsync`'s XML remarks (and in `LLM/scripting.md` § "Exit codes from scripts" — "Returned int becomes the script-host's exit code") was a contract the implementation had never been honoring. Every successful script run returned 0 regardless of its return value.
Impact
ScripTree drivers and shell callers can now key red/green status off combridge's exit code without parsing the script's output text for status markers. The FR was filed while building the MergeInstanceMates ScripTree tool — its Python driver was parsing stdout for `ERROR:` / `FAIL` strings to reconstruct the status that should have come through `$?`. Every future status-bearing `.csx` would have needed the same workaround until this shipped.
Precedence
Bridge-level reserved codes (`2` file-not-found, `3` compile error, `4` script exception, `5` host failure) short-circuit before the script's return value is read. The `return N` path is reached ONLY when the script ran to completion. Reserved-code collisions (e.g. a script returning 4) are the author's problem to avoid.
Verified
| Script | Exit code |
|---|---|
| `return 5;` | 5 ✓ |
| `return 42;` | 42 ✓ |
| no `return` statement | 0 ✓ |
| `throw` before `return 99;` | 5 (HOST EXCEPTION precedence) ✓ |
VBScript path unaffected
The `.vbs` host already propagates `WScript.Quit(N)` as the exit code (verified in v0.8.0's smoke test: `WScript.Quit 42 → exit 42`). Only the Roslyn `.csx` path needed the fix.
v0.8.1 — cement null-global skip with empirical crash finding
Empirically verified that v0.8.0's conservative skip of null reference-type globals was the right call. A downstream session asked whether the workaround (use `If Not IsObject(swDrawing)` instead of the idiomatic `If swDrawing Is Nothing`, and give up `Option Explicit`) was a real constraint. Testing the alternative — register null globals and return `S_OK` + `ppiunkItem=null` from `GetItemInfo` — produced a hard access violation 0xC0000005 inside `vbscript.dll` during `SetScriptState(SCRIPTSTATE_CONNECTED)`. The engine simply dereferences the null dispatch pointer without defending against it.
The docs' "If the item cannot be located, this parameter is set to NULL" wording reads like null might mean Nothing. It does not.
Changes
- Reverted the experimental "register null globals" path back to skip
- Inline code comment updated with the empirical crash finding so the next implementer doesn't re-run the same test
- Skip-list warning now includes the exact workaround pattern: `guard with
If Not IsObject(<name>), notIf <name> Is Nothing` - personal_rag lesson updated with the crash result (previously speculative)
The host's three contracts (now empirically locked)
- Value-type globals skipped — boxed primitives have no COM identity
- Null reference-type globals skipped — engine crashes on null IUnknown
- `WScript.Arguments` deferred — use `ScriptArgs` channel (this one is ergonomic, not a hard constraint)
Downstream scripts must work around all three. The first two are dictated by the IActiveScript/COM contract; only the third is a deliberate combridge scope reduction.
v0.8.0 — VBScript scripting host (run existing SolidWorks/Office VBA macros via .vbs)
Implements FR_vbscript_scripting_host.md. Adds a second script engine to run-script so .vbs files run against the same plugin globals (swApp/swDoc/xlApp/etc.) the Roslyn .csx host exposes. Same --session attach, same output capture, same exit-code mapping.
Why
SolidWorks's entire automation ecosystem is VBA. Forcing C# rewrites blocks every forum macro, every recorded macro, every shop's library from joining the combridge ecosystem. VBScript late binding also sidesteps the entire out/ref PIA quirk family the personal_rag documents — for SW automation specifically, VBScript is MORE robust than typed C# interop, not less.
Use it
' my_macro.vbs — no GetObject, no CreateObject. combridge already attached.
WScript.Echo "SolidWorks version: " & swApp.RevisionNumber
If swDoc Is Nothing Then WScript.Echo "no doc": WScript.Quit 0
WScript.Echo "Active doc: " & swDoc.GetTitlecombridge solidworks run-script my_macro.vbs -
combridge solidworks --session pid:18472 run-script my_macro.vbs -
Extension dispatch
| Extension | Engine |
|---|---|
.csx |
Roslyn C# (existing, unchanged) |
.vbs |
New IActiveScript-hosted VBScript |
.vba / .bas / .swp |
Rejected with conversion-path hint (VBA ≠ VBScript) |
Exit codes (parallel to .csx)
| Code | Meaning |
|---|---|
0 |
success (or WScript.Quit 0) |
2 |
script not found |
3 |
parse error |
4 |
runtime error (Err.Raise, division by zero, COM exception) |
5 |
host failure (e.g. VBScript removed from this Windows) |
| any other | value from WScript.Quit(N) |
Implementation highlights
- Raw
IActiveScript+IActiveScriptParse64(FR Option B). No msscript.ocx dependency; 64-bit clean. - Reflection-based globals injection. The host site enumerates the plugin's globals object's public properties and registers each as a named script item. Works equally for
SwGlobals,ExcelGlobals,WdGlobals, etc. - Honest skip lists. Value-type properties (boxed primitives/enums) and null reference properties can't go through
IActiveScriptSite::GetItemInfoas IUnknown — both are skipped with a host-side warning above script output so authors aren't mystified. - Phase detection via
OnEnterScript(notOnStateChange(CONNECTED), which fires too late — caught and fixed via live division-by-zero test). WScript.Echo/WScript.Quitshim for cscript-compat.WScript.Argumentsdeliberately deferred to keep v0.8.0 scoped (useScriptArgslike.csx).
VBScript deprecation — honest disclosure
Microsoft formally deprecated VBScript in 2024 with planned removal from a future Windows release. This host depends on the in-box vbscript.dll. When Microsoft removes it, CoCreateInstance will return REGDB_E_CLASSNOTREG and this command will exit 5 with a hint pointing at .csx. Building on a deprecated runtime is the right call here (the existing VBA corpus is too valuable to leave unintegrated while we still can), but consumers should know they're investing in a runtime with a known sunset.
Future engines
ActiveScriptInterop.cs declares CLSIDs for both VBScript and JScript. Adding a JScript host is a one-line extension dispatcher + CLSID swap. PowerShell would be a different hosting story (Runspace, not IActiveScript) — future FR.
See CHANGELOG.md v0.8.0 entry for full implementation detail.
v0.7.0 — Outlook search v2 (multi-term, word matching, sender, EntryID) + new outlook get
Implements FR_outlook_search_v2_multiterm_sender_match_and_get.md in full on both Windows and macOS. All 6 items shipped.
Breaking change in outlook search output: matched, entryid, storeid columns are now always emitted (no longer flag-gated), and defaults changed where the old default was wrong. Wrappers built against v0.6.x output need their column expectations updated.
Why withdraw v0.4.0 behavior
The v0.4.0 search was single-term, substring-only, subject/body-only, since-only, no EntryID. The FR documented a real "cast a wide net" task (a Gasspring.ca US$313.89 order across two mailboxes) where every one of those gaps blocked progress — including pdac matching inside a base64 URL blob to surface a Sudbury meal newsletter as a "hit." Modern marketing mail makes that systematic, not accidental.
Defaults changed (FR § "What I'd ship instead")
| Setting | Old | New | Why |
|---|---|---|---|
--match |
implicit substring | word (ci_phrasematch + LIKE+regex fallback) |
substring is just wrong for marketing mail |
--fields |
subject,body |
subject,body,from |
sender search is what you usually want |
| EntryID/StoreID | flag-gated | always emitted | connects search → get without a flag |
Added — outlook search v2
- Multi-term
--query(repeatable + comma-separated) --fields from(sender display name + SMTP address)--match word|substring(word default uses ci_phrasematch with auto-fallback for non-indexed stores)--until yyyy-MM-dd(closed date window with--since)matchedcolumn (per-hit term attribution)entryid+storeidcolumns (always)
Added — outlook get (new command)
combridge outlook get --id <EntryID> [--store <substr>] [--headers] [--html] <out>
combridge outlook get --subject <substr> [--store <substr>] [--folder <substr>] [--max N] <out>
Headers + [BODY] + optional [HTMLBODY] + attachments list.
Live-verified
The FR's exact Gasspring scenario, against live mailboxes:
combridge outlook search --query "ace control,acecontrols,313.89,gasspring,forklift,spring" \
--match word --since 2026-02-01 --until 2026-03-31 --snippet /tmp/gasspring.tsv
- 14 hits vs the FR's 4,412-hit substring flood (99.7% noise reduction)
- First hit =
Your Gasspring.ca order WS14080CA has been received!,matched=313.89,gasspring,spring outlook get --id <EntryID> --store toprops --headersthen returned headers + theMountingDrawing_WS14080CA_8-19-160_8880.pdfattachment + line items + Total: US$313.89
Mac parity
Both OlMacSearchCommand and new OlMacGetCommand ship with the same flag surface and column shape. AppleScript whose is substring-only (no ci_phrasematch), so word-mode always uses the C#-side \bterm\b regex post-filter. EntryID column is the integer AppleScript exposes; StoreID column reuses account name (Mac Outlook has no StoreID concept) — cross-OS schema compatibility preserved. Still classic Outlook for Mac only (not the 2024+ Catalyst "New Outlook").
See CHANGELOG.md v0.7.0 entry for full implementation detail.
v0.6.0 — list-addins on every plugin
Adds a universal list-addins diagnostic subcommand to every plugin: Excel, Word, PowerPoint, Outlook, SolidWorks (Windows) plus best-effort Excel.Mac and Word.Mac. Same category as list-sessions / info — machine-parsable TSV, no business logic, no contract changes.
Why
Every Office/SW consumer was rediscovering 10-30 lines of COM/registry enumeration to answer "is this addin loaded?" Each app has its own non-obvious enumeration model. Shipping a built-in turns N rediscoveries into one canonical answer with a stable shape.
Output shape (consistent across plugins)
# columns: name<TAB>id<TAB>loaded<TAB>kind<TAB>description
<name> <id> <true|false> <COM|XLL|VBA|WLL|TEMPLATE|NATIVE> <extra>
...
# total: <N>
Header rows prefixed with # for easy grep/awk filtering.
Highlights
- Office plugins merge
COMAddIns+AddInsinto one stream with akindcolumn. Each collection wrapped in its own try/catch — partial failures still emit the other. - SolidWorks walks BOTH the UI-visible HKLM tree AND the per-version hidden tree (Design Checker, Costing, TolAnalyst, Sustainability, Reveng, etc.) that Tools→Add-Ins doesn't show. Resolves .NET-hosted
mscoree.dll→CodeBaseURLs automatically. Probes live-load state viaISldWorks.GetAddInObject(Clsid). Per-user enabled-at-startup fromHKCU\Software\SolidWorks\AddInsStartup\{guid}\(Default)REG_DWORD. - Mac plugins (Excel/Word) use AppleScript best-effort — strict subset of Windows (no COMAddIns/XLL/WLL on macOS) but same TSV column shape so cross-OS ScripTree apps work unchanged.
Live-verified
excel list-addins: 8 addins enumerated (Power Map, Power Pivot, Acrobat PDFMaker, Data Streamer, Analysis ToolPak XLL+VBA, Solver, Euro Tools).solidworks list-addins: 25 addins on SW 2026 SP1.1 — exactly matching the dual-registry lesson's predictions, including the FuncFeatApp lazy-load case.
Not enumerable
SLDWORKS.EXE-hardcoded modules (fworks.dll, swbrowser.dll, etc.) appear in neither registry tree and can't be toggled. Output's trailing # total: line notes this.
See CHANGELOG.md v0.6.0 entry for full implementation detail.
v0.5.0 — withdraw v0.4.2 alias preamble; ship visible scaffolding + smart CS0104 hints
Breaking change (justifies the minor-version bump): IComBridgePlugin.ScriptUsingAliases removed. Plugins built against v0.4.2 that relied on the preamble mechanism need rebuilding against v0.5.0; the four Office plugins shipped in this repo are already updated.
Why withdraw v0.4.2
The v0.4.2 mechanism injected using Xl = global::...; into the script source before Roslyn compiled it. That solved CS0104 — but at app-store scale (thousands of plugins, thousands of authors, public ScripTree catalog), invisible source rewriting breaks more than it fixes:
- External IDEs (VS Code, Rider, Cursor) can't see the preamble → red squiggles on working scripts
- LLMs reading the .csx in isolation hallucinate where
Xlcame from - App-store auditors can't evaluate published scripts without learning host internals
- Roslyn diagnostic-format changes could silently break line-number remap
- "Rewrite the script before compile" is a category of mechanism that grows
The savings (1 line of typing per script, amortized away by templates anyway) didn't justify those costs.
What ships in v0.5.0
Two visible tools that solve the same problem without host-side rewriting:
<plugin> new-script <path> [--force]
Scaffolds a starter .csx with the alias line, available globals documented in a header comment, and a minimal example body. The alias is right there in the source file — every reader sees what's in scope.
combridge excel new-script my_thing.csx
combridge word new-script my_thing.csx
combridge powerpoint new-script my_thing.csx
combridge outlook new-script my_thing.csx
Smart CS0104 hints
If you forget the alias and hit error CS0104: 'Range' is an ambiguous reference between ..., the host detects the Office-interop / BCL collision pattern and appends a hint:
collision_test.csx(2,1): error CS0104: 'Range' is an ambiguous reference between 'Microsoft.Office.Interop.Word.Range' and 'System.Range'
-> Hint: add this to the top of your script:
using Wd = global::Microsoft.Office.Interop.Word;
then use 'Wd.Range' instead of bare 'Range',
or qualify the BCL side as 'System.Range'.
See LLM/scripting.md for the full collision table.
Original diagnostic preserved verbatim — including the author's actual line/column span. No source rewriting, no remapping.
Removed
IComBridgePlugin.ScriptUsingAliasesScriptHost's preamble injectionScriptHost.DetectEncoding+RemapDiagnosticLine+DiagLocRxScriptUsingAliasesoverrides on the four Office plugins
Added
ScriptScaffold.WriteTemplatehelper inComBridge.Core(shared by allnew-scriptcommands)NewScriptCommandon each Windows Office pluginScriptHost.AugmentOfficeDiagnostic(additive, no source mutation)
See CHANGELOG.md v0.5.0 entry for full implementation detail; see FeatureRequests/ComBridge_FeatureRequests/Rejected/FR_office_script_interop_alias.md for the rejection rationale and lessons-captured note.
v0.4.2 — auto-provided Office interop aliases
Implements FR_office_script_interop_alias.md in full. Fixes the CS0104 ambiguous-reference papercut that hit every Windows Office .csx: Range, Exception, Application, Style, Font, Action, Page collide between the auto-imported interop namespace and the BCL. Range was the worst because modern C# added System.Range, so the common idiom Range used = xlSheet.UsedRange; failed until each author re-typed using Xl = global::Microsoft.Office.Interop.Excel; at the top of every script.
Added
IComBridgePlugin.ScriptUsingAliases— new optional contract member. Plugins return alias bodies (e.g."Xl = global::Microsoft.Office.Interop.Excel"); the host renders them asusing <alias>;directives prepended to the script source. Default = empty, so non-Office plugins (SolidWorks, all Mac plugins) are zero-impact.- Alias preamble in
ScriptHost.RunAsync— concatenates each plugin's aliases onto a single first line, preserves BOM + encoding so PDB emit still works (CS8055-free). - Diagnostic line-number remapping —
(LINE,COL)spans in Roslyn errors are rewritten back to the author's real source. Errors located in the preamble itself are left untouched so plugin-author bugs surface loudly. - Four Windows Office plugins now contribute their alias —
excel→Xl,word→Wd,powerpoint→Pp,outlook→Ol.
Use it
// In any Word .csx — `Wd` is now auto-available, no `using Wd = ...` needed
Wd.Range rng = wdDoc.Content;
Wd.Style heading = wdDoc.Styles[Wd.WdBuiltinStyle.wdStyleHeading1];
// In any Excel .csx
Xl.Range used = xlSheet.UsedRange;
foreach (Xl.Range row in used.Rows) { /* ... */ }Deliberately NOT changed
- Bare
Rangestays ambiguous. The alias only guarantees a reliable qualifier (Xl.Range,Wd.Range) is always in scope; it does NOT silently pick the Office type overSystem.Range. System.Exceptionstill needs to be qualified. The aliases fix the Office-side qualifier, not the BCL side.- Existing scripts that already declare
using Xl = …keep working — Roslyn quietly accepts the duplicate.
Docs
LLM/scripting.md§ Office namespace shadowing rewritten to lead with the auto-alias.LLM/troubleshooting.mdgained a dedicated CS0104 entry.- See
CHANGELOG.mdfor full implementation detail.