From d998131678088e32c231b5c6b4d087cdeabc26d2 Mon Sep 17 00:00:00 2001 From: WillowDev <216433317+willowdev-code@users.noreply.github.com> Date: Tue, 27 Oct 2026 14:00:19 +0100 Subject: [PATCH] ADO Lens 0.11.4 The environments are set the same way in the window, the terminal and the installer. - `adolens setup` (terminal): names in use are kept with Enter and removed with '-'; what was found gets no name until one is typed - '+' takes the hint ("looks like DEV"), Enter skips. With more than 8 found, it first asks which to go through: all, only TFVC branches and release stages ('-'), or repositories / text. - Installer, environments page: found rows start empty with the hint next to them ("Main [DEV]"), a repository filter (all / TFVC branches and release stages / one repository) and "Use the hints for the rows shown". Rows hidden by the filter keep what was typed and are saved too. --- CHANGELOG.md | 11 +++ packaging/inno/ado-lens.iss | 142 +++++++++++++++++++++++++++++------- src/AdoLens/AdoLens.csproj | 2 +- src/AdoLens/Cli.cs | 44 ++++++++--- 4 files changed, 161 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b047ec..6270420 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 0.11.4 — 2026-10-27 + +The environments are set the same way in the window, the terminal and the installer. + +- `adolens setup` (terminal): names in use are kept with Enter and removed with '-'; what was found gets no name until + one is typed - '+' takes the hint ("looks like DEV"), Enter skips. With more than 8 found, it first asks which to go + through: all, only TFVC branches and release stages ('-'), or repositories / text. +- Installer, environments page: found rows start empty with the hint next to them ("Main [DEV]"), a repository + filter (all / TFVC branches and release stages / one repository) and "Use the hints for the rows shown". Rows hidden + by the filter keep what was typed and are saved too. + ## 0.11.3 — 2026-10-27 - Finding the environments includes TFVC branches ($/Project/Trunk/Main, Release, SIT…) with a changeset in the last diff --git a/packaging/inno/ado-lens.iss b/packaging/inno/ado-lens.iss index 7506b7c..2e9dad8 100644 --- a/packaging/inno/ado-lens.iss +++ b/packaging/inno/ado-lens.iss @@ -84,7 +84,7 @@ Filename: "{cm:ReadmeUrl}"; Description: "{cm:OpenReadme}"; Flags: postinstall s [CustomMessages] en.EnvTitle=Environments en.EnvSub=Where code goes before it reaches production -en.EnvText=ADO Lens found these release stages and branches. Name each environment the way your team says it (for example UAT (sbx03)); leave a field empty to skip it. +en.EnvText=ADO Lens found these release stages and branches; [DEV] is a hint. Name the ones that are environments the way your team says it (for example UAT (sbx03)); an empty field is skipped. en.EnvDetecting=Looking for release stages and branches... en.EnvNone=No release stages or long-lived branches were found. Claude will ask about environments when it needs them. en.EnvExisting=Environments are already set: %1 (%2). @@ -93,6 +93,9 @@ en.EnvMine=Save for me only (this computer) en.EnvTeam=Save for the whole team (wiki page "ADO Lens" of the project) en.EnvSkip=Skip - Claude asks when needed en.EnvMore=... and %1 more: Start menu > ADO Lens - settings > Environments. +en.EnvAll=(all) +en.EnvNoRepo=(TFVC branches and release stages) +en.EnvHints=Use the hints for the rows shown en.EnvFailed=Saving the environments did not work:%n%n%1 en.ReadmeUrl=https://github.com/willowdev-code/ado-lens#readme en.IconSettings=ADO Lens - settings @@ -101,7 +104,7 @@ en.IconSkills=ADO Lens - skills for Claude Desktop en.RemoveData=Also remove the stored personal access tokens (Windows Credential Manager) and the settings and cache of ADO Lens?%n%nChoose No to keep them for a later installation. pl.EnvTitle=Środowiska pl.EnvSub=Gdzie trafia kod, zanim dojdzie na produkcję -pl.EnvText=ADO Lens znalazł te etapy release'ów i gałęzie. Nazwij każde środowisko tak, jak mówi o nim zespół (np. UAT (sbx03)); puste pole = pomiń. +pl.EnvText=ADO Lens znalazł te etapy release'ów i gałęzie; [DEV] to podpowiedź. Nazwij te, które są środowiskami, tak jak mówi o nich zespół (np. UAT (sbx03)); puste pole = pomiń. pl.EnvDetecting=Szukam etapów release'ów i gałęzi... pl.EnvNone=Nie znaleziono etapów release'ów ani gałęzi. Claude zapyta o środowiska, gdy będą potrzebne. pl.EnvExisting=Środowiska są już ustawione: %1 (%2). @@ -110,6 +113,9 @@ pl.EnvMine=Zapisz tylko dla mnie (ten komputer) pl.EnvTeam=Zapisz dla całego zespołu (strona wiki "ADO Lens" w projekcie) pl.EnvSkip=Pomiń - Claude zapyta, gdy będzie trzeba pl.EnvMore=... i jeszcze %1: menu Start > ADO Lens - ustawienia > Środowiska. +pl.EnvAll=(wszystkie) +pl.EnvNoRepo=(gałęzie TFVC i etapy release'ów) +pl.EnvHints=Użyj podpowiedzi dla widocznych pl.EnvFailed=Nie udało się zapisać środowisk:%n%n%1 pl.ReadmeUrl=https://github.com/willowdev-code/ado-lens/blob/main/README.pl.md pl.IconSettings=ADO Lens - ustawienia @@ -173,7 +179,10 @@ var HasConnection: Boolean; EnvPage: TWizardPage; EnvInfo, EnvLater: TNewStaticText; - EnvKeys: TArrayOfString; + EnvKeys, EnvGuess, EnvRepo: TArrayOfString; + EnvFilter: TNewComboBox; + EnvHints: TNewButton; + EnvRowsTop: Integer; EnvLabels: array of TNewStaticText; EnvEdits: array of TNewEdit; EnvScope: TNewComboBox; @@ -419,25 +428,94 @@ begin EnvMore.Visible := False; end; +{ Shows the rows that match the filter, as many as fit between the filter and the drop-down; the rest is counted. + Hidden rows keep what was typed and are saved too. } +procedure LayoutEnvRows; +var + I, Shown, Hidden, MaxRows: Integer; + Match: Boolean; +begin + MaxRows := (EnvMore.Top - EnvRowsTop) div ScaleY(26); + Shown := 0; + Hidden := 0; + for I := 0 to GetArrayLength(EnvKeys) - 1 do + begin + if EnvFilter.ItemIndex <= 0 then Match := True + else if EnvFilter.ItemIndex = 1 then Match := EnvRepo[I] = '' + else Match := CompareText(EnvRepo[I], EnvFilter.Items[EnvFilter.ItemIndex]) = 0; + if Match and (Shown < MaxRows) then + begin + EnvLabels[I].Top := EnvRowsTop + Shown * ScaleY(26) + ScaleY(3); + EnvEdits[I].Top := EnvRowsTop + Shown * ScaleY(26); + EnvLabels[I].Visible := True; + EnvEdits[I].Visible := True; + Shown := Shown + 1; + end + else + begin + if Match then Hidden := Hidden + 1; + EnvLabels[I].Visible := False; + EnvEdits[I].Visible := False; + end; + end; + EnvMore.Visible := Hidden > 0; + if Hidden > 0 then EnvMore.Caption := FmtMessage(CustomMessage('EnvMore'), [IntToStr(Hidden)]); +end; + +procedure EnvFilterChange(Sender: TObject); +begin + LayoutEnvRows; +end; + +{ As in the settings window: a row found gets no name until given; the hint is shown next to it. } +procedure EnvHintsClick(Sender: TObject); +var + I: Integer; +begin + for I := 0 to GetArrayLength(EnvKeys) - 1 do + if EnvEdits[I].Visible and (Trim(EnvEdits[I].Text) = '') and (EnvGuess[I] <> '') then + EnvEdits[I].Text := EnvGuess[I]; +end; + { Fills the environment page from "adolens setup --detect": the map already in use, or one row per stage or branch - found with a suggested name. } + found, with its hint. } procedure DetectEnvironments; var - Exe, OutFile, Line, Key, Guess, Caption: String; + Exe, OutFile, Line, Key, Guess, Caption, Repo: String; Lines: TArrayOfString; - I, P, N, Top, MaxRows, Skipped: Integer; + I, P, N, Repos: Integer; Code: Integer; begin - { As many rows as fit between the text and the drop-down; the rest is named and can be set in the settings later. } - MaxRows := (EnvMore.Top - (EnvInfo.Top + EnvInfo.Height + ScaleY(4))) div ScaleY(26); - Skipped := 0; EnvDetected := True; Exe := ExpandConstant('{tmp}\adolens.exe'); OutFile := ExpandConstant('{tmp}\adolens-envs.txt'); DeleteFile(OutFile); Exec(Exe, 'setup --detect --out "' + OutFile + '"', '', SW_HIDE, ewWaitUntilTerminated, Code); if not LoadStringsFromFile(OutFile, Lines) then SetArrayLength(Lines, 0); + + { The filter and the hints button, under the text. } + EnvFilter := TNewComboBox.Create(EnvPage); + EnvFilter.Parent := EnvPage.Surface; + EnvFilter.Style := csDropDownList; + EnvFilter.Top := EnvInfo.Top + EnvInfo.Height + ScaleY(2); + EnvFilter.Width := EnvPage.SurfaceWidth div 2 - ScaleX(8); + EnvFilter.Items.Add(CustomMessage('EnvAll')); + EnvFilter.Items.Add(CustomMessage('EnvNoRepo')); + EnvFilter.OnChange := @EnvFilterChange; + EnvFilter.Visible := False; + EnvHints := TNewButton.Create(EnvPage); + EnvHints.Parent := EnvPage.Surface; + EnvHints.Caption := CustomMessage('EnvHints'); + EnvHints.Left := EnvPage.SurfaceWidth div 2; + EnvHints.Top := EnvFilter.Top; + EnvHints.Width := EnvPage.SurfaceWidth div 2; + EnvHints.Height := EnvFilter.Height; + EnvHints.OnClick := @EnvHintsClick; + EnvHints.Visible := False; + EnvRowsTop := EnvFilter.Top + EnvFilter.Height + ScaleY(6); + N := 0; + Repos := 0; SetArrayLength(EnvKeys, 0); for I := 0 to GetArrayLength(Lines) - 1 do begin @@ -449,9 +527,7 @@ begin EnvInfo.Caption := FmtMessage(CustomMessage('EnvExisting'), [Copy(Line, 1, P - 1), Copy(Line, P + 1, Length(Line))]); Exit; end; - if (Copy(Line, 1, 4) = 'row' + #9) and (N >= MaxRows) then - Skipped := Skipped + 1 - else if Copy(Line, 1, 4) = 'row' + #9 then + if (Copy(Line, 1, 4) = 'row' + #9) and (N < 80) then begin Delete(Line, 1, 4); P := Pos(#9, Line); @@ -465,30 +541,41 @@ begin Key := Line; Guess := ''; end; + { "D365FO:staging" is the branch staging of the repository D365FO. } + Repo := ''; + Caption := Key; + P := Pos(':', Key); + if (P > 1) and (Pos('/', Key) = 0) then + begin + Repo := Copy(Key, 1, P - 1); + Caption := Copy(Key, P + 1, Length(Key)) + ' (' + Repo + ')'; + if EnvFilter.Items.IndexOf(Repo) < 0 then + begin + EnvFilter.Items.Add(Repo); + Repos := Repos + 1; + end; + end; + if Guess <> '' then Caption := Caption + ' [' + Guess + ']'; N := N + 1; SetArrayLength(EnvKeys, N); + SetArrayLength(EnvGuess, N); + SetArrayLength(EnvRepo, N); SetArrayLength(EnvLabels, N); SetArrayLength(EnvEdits, N); EnvKeys[N - 1] := Key; - Top := EnvInfo.Top + EnvInfo.Height + ScaleY(4) + (N - 1) * ScaleY(26); + EnvGuess[N - 1] := Guess; + EnvRepo[N - 1] := Repo; EnvLabels[N - 1] := TNewStaticText.Create(EnvPage); EnvLabels[N - 1].Parent := EnvPage.Surface; - { "D365FO:staging" is the branch staging of the repository D365FO. } - Caption := Key; - P := Pos(':', Key); - if (P > 1) and (Pos('/', Key) = 0) then - Caption := Copy(Key, P + 1, Length(Key)) + ' (' + Copy(Key, 1, P - 1) + ')'; EnvLabels[N - 1].Caption := Caption; EnvLabels[N - 1].AutoSize := False; EnvLabels[N - 1].Height := ScaleY(18); - EnvLabels[N - 1].Top := Top + ScaleY(3); EnvLabels[N - 1].Width := EnvPage.SurfaceWidth div 2 - ScaleX(8); EnvEdits[N - 1] := TNewEdit.Create(EnvPage); EnvEdits[N - 1].Parent := EnvPage.Surface; EnvEdits[N - 1].Left := EnvPage.SurfaceWidth div 2; - EnvEdits[N - 1].Top := Top; EnvEdits[N - 1].Width := EnvPage.SurfaceWidth div 2; - EnvEdits[N - 1].Text := Guess; + EnvEdits[N - 1].Text := ''; end; end; if N = 0 then @@ -497,11 +584,14 @@ begin Exit; end; EnvInfo.Caption := CustomMessage('EnvText'); - if Skipped > 0 then - begin - EnvMore.Caption := FmtMessage(CustomMessage('EnvMore'), [IntToStr(Skipped)]); - EnvMore.Visible := True; - end; + { With several repositories, start with the TFVC branches and release stages when there are any. } + EnvFilter.ItemIndex := 0; + if Repos > 0 then + for I := 0 to N - 1 do + if EnvRepo[I] = '' then EnvFilter.ItemIndex := 1; + EnvFilter.Visible := True; + EnvHints.Visible := True; + LayoutEnvRows; EnvScope.Visible := True; end; diff --git a/src/AdoLens/AdoLens.csproj b/src/AdoLens/AdoLens.csproj index e6106c9..7fb6ab3 100644 --- a/src/AdoLens/AdoLens.csproj +++ b/src/AdoLens/AdoLens.csproj @@ -25,7 +25,7 @@ ADO Lens for Azure DevOps Azure DevOps (work items, boards, wiki, Git and TFVC, pipelines) served to AI assistants over MCP Copyright © 2026 WillowDev - 0.11.3 + 0.11.4 diff --git a/src/AdoLens/Cli.cs b/src/AdoLens/Cli.cs index 844f074..e176385 100644 --- a/src/AdoLens/Cli.cs +++ b/src/AdoLens/Cli.cs @@ -678,11 +678,8 @@ async Task EnvironmentsAsync(string name, Session session, string project, bool Console.WriteLine(Ui.T($"{name} / {project}: code goes {string.Join(" -> ", effective.Values)} ({source}).", $"{name} / {project}: kod trafia kolejno na {string.Join(" -> ", effective.Values)} ({source}).")); if (effective.Count > 0 && !change) return; - var detected = await EnvironmentFlow.DetectAsync(session, project, default); - // Rows: the current map in its order, then what was found and is not in it yet. - var rows = effective.Select(e => (e.Key, Guess: (string?)e.Value)) - .Concat(detected.Where(d => !effective.ContainsKey(d.Key))).ToList(); - if (rows.Count == 0 || !interactive) + var found = (await EnvironmentFlow.DetectAsync(session, project, default)).Where(d => !effective.ContainsKey(d.Key)).ToList(); + if ((effective.Count == 0 && found.Count == 0) || !interactive) { Console.WriteLine(Ui.T($"{name} / {project}: no release stages or long-lived branches found - Claude will ask when needed.", $"{name} / {project}: nie znaleziono etapów release'ów ani gałęzi - Claude zapyta, gdy będzie trzeba.")); @@ -690,15 +687,40 @@ async Task EnvironmentsAsync(string name, Session session, string project, bool } Console.WriteLine(Ui.T($"{name} / {project}: where does code go before it reaches production?", $"{name} / {project}: przez jakie środowiska przechodzi kod, zanim trafi na produkcję?")); - Console.WriteLine(Ui.T("Name each as people say it (e.g. UAT (sbx03)); Enter keeps [the name], '-' leaves it out.", - "Nadaj każdemu nazwę, której używacie (np. UAT (sbx03)); Enter zostawia [nazwę], '-' pomija.")); var map = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var (key, guess) in rows) + // 1. The map in use: Enter keeps a name, '-' removes it. + if (effective.Count > 0) + Console.WriteLine(Ui.T("In use - Enter keeps [the name], '-' removes it:", "W użyciu - Enter zostawia [nazwę], '-' usuwa:")); + foreach (var (key, current) in effective) { - var label = Prompt($" {key}{(guess is null ? "" : $" [{guess}]")}: "); + var label = Prompt($" {key} [{current}]: "); if (label == "-") continue; - var value = string.IsNullOrWhiteSpace(label) ? guess : label.Trim(); - if (value is not null) map[key] = value; + map[key] = string.IsNullOrWhiteSpace(label) ? current : label.Trim(); + } + // 2. What was found, as in the settings window: no name until given, so a project with many repositories does + // not save every branch. Many rows: narrow them down first. + if (found.Count > 8) + { + var repos = found.Select(f => EnvMap.RepoOf(f.Key)).OfType().Distinct(StringComparer.OrdinalIgnoreCase).Order(StringComparer.OrdinalIgnoreCase).ToList(); + Console.WriteLine(Ui.T($"Found {found.Count}: {found.Count(f => EnvMap.RepoOf(f.Key) is null)} TFVC branches or release stages, the rest in {repos.Count} repositories ({string.Join(", ", repos.Take(12))}{(repos.Count > 12 ? ", …" : "")}).", + $"Znaleziono {found.Count}: gałęzie TFVC lub etapy release'ów: {found.Count(f => EnvMap.RepoOf(f.Key) is null)}, reszta w {repos.Count} repozytoriach ({string.Join(", ", repos.Take(12))}{(repos.Count > 12 ? ", …" : "")}).")); + var only = Prompt(Ui.T(" go through: Enter = all, '-' = only TFVC branches and release stages, or repositories / text (e.g. D365FO): ", + " które przejrzeć: Enter = wszystkie, '-' = tylko gałęzie TFVC i etapy, albo repozytoria / tekst (np. D365FO): "))?.Trim(); + if (only == "-") found = found.Where(f => EnvMap.RepoOf(f.Key) is null).ToList(); + else if (!string.IsNullOrEmpty(only)) + { + var words = only.Split([',', ';', ' '], StringSplitOptions.RemoveEmptyEntries); + found = found.Where(f => words.Any(w => f.Key.Contains(w, StringComparison.OrdinalIgnoreCase))).ToList(); + } + } + if (found.Count > 0) + Console.WriteLine(Ui.T("Found - type the name as people say it (e.g. UAT (sbx03)), '+' takes the hint, Enter skips:", + "Znalezione - wpisz nazwę, jakiej używacie (np. UAT (sbx03)), '+' przyjmuje podpowiedź, Enter pomija:")); + foreach (var (key, hint) in found) + { + var label = Prompt($" {key}{(hint is null ? "" : Ui.T($" (looks like {hint})", $" (wygląda na {hint})"))}: ")?.Trim(); + if (label == "+" && hint is not null) map[key] = hint; + else if (!string.IsNullOrEmpty(label) && label != "+" && label != "-") map[key] = label; } // Something the search did not find (a release stage that has not run yet, a branch in another project). while (Prompt(Ui.T(" another one (branch or Release/Stage=name, Enter = done): ", " kolejne (gałąź albo Release/Etap=nazwa, Enter = koniec): ")) is { Length: > 0 } extra)