Skip to content

Refactor/project layout - #66

Open
lsoft wants to merge 19 commits into
mainfrom
refactor/project-layout
Open

Refactor/project layout#66
lsoft wants to merge 19 commits into
mainfrom
refactor/project-layout

Conversation

@lsoft

@lsoft lsoft commented Aug 9, 2026

Copy link
Copy Markdown
Owner

No description provided.

lsoft and others added 19 commits August 9, 2026 12:03
FreeAIr.csproj listed all 279 source files by hand, which silently drops a
file that nobody remembers to add. It is SDK-style now and globs its sources:
660 lines down to ~120.

Two assemblies come out of it:

- FreeAIr.Shared gains CallAwaiter, NonDisposableSemaphoreSlim, TempFile and
  ActivityLogHelper, none of which had any reason to sit in the VSIX;
- FreeAIr.Voice takes the whole Record/ folder and the recording options page,
  and with them six packages (NAudio, Whisper.net and its two runtimes,
  System.Speech, Windows.SDK.Contracts) plus the whisper runtime zipping,
  which is now a proper AfterTargets="Build" target instead of a
  PostBuildEvent that never saw $(TargetDir).

CLAUDE.md records the two traps this hit: MSBuild-only builds, and F5 being
driven by AdditionalArguments now that dropping the project flavor handed the
Debug page to Visual Studio's own extensibility project system.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
InfoBarService.ShowInfoBar returned silently when the main window had no
info bar host, which is exactly the state devenv is in while the start
window is up - so on a machine that opens no solution the "new version
installed" and first-run wizard bars were simply dropped. Waiting for
ShellInitializedContext does not help either: the shell reports itself
initialized long before the main window exists. It now retries once a
second for a minute and gives up with a log line rather than in silence.

The two callers log what they decided, which is what made this findable in
the activity log at all.

Dismissing the first-run bar also records FreeAIrLastVersion now. That bar
links to the release notes itself, so without it a brand new install got the
plain "new version installed" bar on its very next start.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…onEdit

The parsed-prompt model (IParsedPart, Parsed, the answer parts,
SelectedIdentifier) has no WPF in it and is used by Chat, Helper, Git,
Commands and NLOutline, yet it lived under UI/Embedillo/. The files move to
Chat/Context/Parser/; the namespace deliberately stays as it was, so this
commit changes no using directive anywhere.

IParser was the real coupling: it published its mentions as
MentionVisualLineGenerator, an AvalonEdit VisualLineElementGenerator, so the
whole ANTLR pipeline dragged in the editor. The parsers only ever used two
members of it, which are now IMentionRecognizer. The generator implements
that interface without a line of its body changing, IParser.Generators is
gone, and the chat control passes the generators to the editor itself -
the parser has no business handing out editor objects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Most of what looked like a UI dependency was a misfiled type. BackgroundTask
sat inside WaitForTaskWindow.xaml.cs and DifferenceShower under UI/Difference,
yet neither has a line of WPF in it - the first is a cancellable task with a
status event, the second drives IVsDifferenceService. Both move out; the
window keeps only the window. GitWindowModifier makes the opposite trip: it
is pure WPF and lived in Git/.

What genuinely needed a seam now has one in Interaction/, implemented in the
UI assembly and resolved through MEF the way ChatContainer already was:

- IBackgroundTaskShower puts up the wait dialog, so the git diff collector
  no longer constructs a Window;
- IUserChooser asks for a support action and an agent, hiding the Visual
  Studio context menus behind the two questions every command starts with;
- IGitCommitMessageBox is the Git Changes commit box as far as the commit
  message builder cares, implemented by GitWindowModifier;
- IChatWindowShower shows a chat wherever chats go;
- IChatStatusIndicator takes the aggregate chat status, and ChatsStatusEnum
  moves with it, so ChatContainer no longer imports the status bar control.

UIInformer and GitWindowModifier now carry two exports each and are marked
Shared explicitly - both must stay singletons, since the package initializes
the one instance the rest of the extension reports into.

Git/GitNaturalLanguageOutliner still reaches for NaturalLanguageOutlinesViewModel.
That is a real dependency on the NLO feature rather than a presentation seam,
and belongs to the wave which extracts it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
NLO is one feature spread over five folders: the outline tree builder, the
embedding index, the natural language search, four view models and four tool
windows, none of which anything else uses. Nothing but the paths changes here
- the namespaces stay where they are, so no using is touched and the diff is
renames only.

  NLOutline/                    -> Nlo/NLOutline/
  Embedding/                    -> Nlo/Embedding/
  Find/                         -> Nlo/Find/
  UI/ViewModels/*NaturalLanguage*, *Rag*, OutlineEmbeddingOutputPanel
                                -> Nlo/ViewModels/
  UI/ToolWindows/ same four     -> Nlo/ToolWindows/

Search/ is not part of it and never was: it is the Google scraper behind the
WebSearch MCP tool, and a folder of that name next to the FreeAIr.Search
project - which holds the outline index and the RAG ranking - is a trap. It
becomes WebSearch/, after the tool that uses it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The one edge left over from the previous wave was Git/GitNaturalLanguageOutliner
reaching into NaturalLanguageOutlinesViewModel to open the outlines panel over
the pending diff. It now goes through INaturalLanguageOutlinesPanel, the single
export Nlo/ publishes to the rest of the extension - implemented in
Nlo/Interaction/ next to the view model it forwards to.

The other direction had three edges left, all of them pickers or a dialog:

- DoSearch asked the context menus directly for the scope, the support action
  and both agents. IUserChooser already answered two of those questions and now
  answers the other two: ChooseAnyAgentAsync, the picker without the has-a-token
  filter that the embedding agent needs, and ChooseOneOfAsync<T> for a plain list
  of labelled values - the search scope, for one.
- RagCalibrationViewModel used the same agent picker; same treatment.
- BuildNaturalLanguageOutlinesJsonFileToolViewModel constructed WaitForTaskWindow
  itself and now uses IBackgroundTaskShower like the git commands do.

What is left pointing from Nlo/ at UI/ is UI/NestedCheckBox, a reusable control
which is equally used by the tools and the control center windows and belongs
wherever the shared WPF controls end up; everything else is a namespace which no
longer matches its folder (UI.ViewModels and UI.ToolWindows are the feature's own
now, UI.Windows is BackgroundTask in BLogic/, UI.Embedillo.Answer.Parser is in
Chat/Context/Parser/).

A separate FreeAIr.Nlo assembly is still not possible: the view models need
Resources, Chat, Options2 and Helper, all of which are in the VSIX assembly, so
the reference would be circular. Nlo/ is the shape it will have when they are not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tool scraped google.com through a headless Chromium: it typed the query
character by character, walked the result pages and parsed titles out of class
names like `LC20lb MBeuO DKV0Md`. That is not something which keeps working,
and the two packages behind it were the price - PuppeteerSharp, which also
downloads a Chromium build on the first call, and HtmlAgilityPack.

Both were referenced by nothing else, so they go too, along with
NaturalLanguageHelper whose only caller was the scraper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Almost every feature folder of the VSIX reaches for Resources.Resources, so
while the .resx files sat inside FreeAIr.dll nothing could be pulled out of it
without referencing the VSIX back. They are now the leaf assembly everything
else may depend on, which is what unblocks the next extraction.

The manifest name stays FreeAIr.Resources.Resources - RootNamespace plus a
.resx at the project root reproduces it exactly - so the generated
ResourceManager and the satellites are unchanged. Only the three .resx files
and Resources1.Designer.cs moved; the icons stay in FreeAIr\Resources\, where
the pack URIs and VSCommandTable.vsct expect them.

Two things this uncovered:

- XAML reads `clr-namespace:` as the current assembly, so all 17 files naming
  the strings needed `;assembly=FreeAIr.Resources` appended. The C# compiled
  fine and the markup compiler failed with MC3050.
- A referenced project's satellites reach the VSIX packer twice, once with the
  culture folder and once without, and the flattened copy shadows every culture
  but the first in the root of the package. RemoveFlattenedSatellites drops it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Thirty-seven places wrote the same line to resolve an exported service:

    (IComponentModel)await FreeAIrPackage.Instance.GetServiceAsync(typeof(SComponentModel))

which tied the natural language outlines, the git commands and the MCP tools
to the package class for no reason beyond that being where somebody first
wrote it. The global async service provider hands out the same
SComponentModel, so MefHelper in Shared does it and belongs to no feature.

Nlo\ no longer names FreeAIrPackage at all, which was the first of the edges
standing between it and an assembly of its own. The two stragglers asking the
package for EnvDTE.DTE now use the same provider.

Shared went SDK-style on the way: MefHelper had to be added to a hand-written
<Compile Include> list otherwise, which is exactly the trap that project was
documented for. CodeLens is now the only legacy-format project left.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four moves, each of them a file sitting somewhere it does not belong:

- UI\NestedCheckBox and the converter its XAML needs go to WpfHelpers. It is a
  general tree-of-checkboxes control, used by the outlines panel, the available
  tools list and the control center alike, and nothing about it is FreeAIr's.
- BLogic\BackgroundTask goes to Shared. It has no UI in it - the wait dialog is
  one possible presenter - and git, MCP and the outlines all derive from it, so
  keeping it in the VSIX pinned them there too.
- Helper\EnumHelper and Helper\TextDescriptorHelper go to Chat\. One formats a
  ChatStatusEnum for display, the other builds a SelectedTextDescriptor; both
  are chat code that happened to be filed under Helper.
- SelectedSpan leaves the parser file for Shared. It is a start offset and a
  length; SolutionHelper needs it, and that was the last thing tying Helper to
  Chat. What remains of the file is SelectedIdentifier, and it is now named so.

Helper\ has no dependency on any other folder left. Nlo\ is down from eight
folders to five - Chat, Options2, Helper, Git and the Interaction seams.

Helper\ResourceHelper.cs is deleted rather than moved: it looked up a resource
by name in the configured answer culture and nothing has ever called it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Options2 depended on six other folders. A model of what is configured has no
business knowing the runtime that acts on it, and while it did, nothing built
on top of it could be moved anywhere.

Five of the six are gone:

- DataPieceCache moves into Options2\. It caches the parsed options file and
  FreeAIrOptions was its only caller; it was never general BLogic.
- ApplyMcpServerNodeAsync moves to McpServerProxyApplication as
  ApplyServerNodeAsync. It starts the configured servers, collects the ones
  that failed and puts up a message box about them - none of which is settings
  code, and it sat right next to the UpdateExternalServersAsync it wraps.
- SupportContext.WithContextItemAsync took a list of SolutionItemChatContextItem
  to read one file path off each, and WithErrorInformationAsync took a
  BuildResultInformation to read four fields off it. Both now take what they
  actually use, which drops Chat and BuildErrors.
- InternalPage and MCPToolsExecutionStatus move into Options2\. InternalPage is
  where the serialized settings are stored - it is FreeAIr's persisted internal
  state and only incidentally a (Browsable(false)) options page. Options\ is
  left with the three pages a user can actually see.

What Options2 still names outside itself is Helper.UriHelper, and Helper has no
dependencies of its own left, so that one points the right way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
OpenAI.dll only surfaces the HTTP status; the JSON body is what names the rejected parameter.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
CallToolRequest carried its arguments as a Dictionary<string, object?>, and the
channel to the proxy is StreamJsonRpc with its default Newtonsoft formatter, so
everything that was not a primitive arrived as a JObject or a JArray. Handing
those to System.Text.Json - the serializer the MCP SDK builds tools/call with -
writes out the token's children rather than its value, because every JToken
implements IEnumerable<JToken>. Nested objects and arrays therefore reached the
MCP server as nested empty arrays, which is why only tools taking flat scalar
arguments appeared to work. Closes #70.

The arguments now cross as raw JSON text, the way the tool schema already
travels back in GetToolReply.Parameters, and are revived as JsonElement on the
proxy side. IServer and BaseServer take that type through to the SDK, so nothing
weakly typed can be passed down that path again.

JsonElementDeserializer moves to MCP/Dto next to the conversion it feeds; its
namespace is unchanged.

The new FreeAIr.Mcp.Tests covers both halves against live components - a real
JsonRpc pair and a real MCP server on an in-process duplex stream - including a
test that asserts a weakly typed member still loses its shape on the channel, so
the reason for the design cannot quietly stop being true.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant