Skip to content

Commit e6ffa36

Browse files
ADO Lens 0.11.4
ADO Lens 0.11.4
2 parents 715d20e + d998131 commit e6ffa36

4 files changed

Lines changed: 161 additions & 38 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Changelog
22

3+
## 0.11.4 — 2026-10-27
4+
5+
The environments are set the same way in the window, the terminal and the installer.
6+
7+
- `adolens setup` (terminal): names in use are kept with Enter and removed with '-'; what was found gets no name until
8+
one is typed - '+' takes the hint ("looks like DEV"), Enter skips. With more than 8 found, it first asks which to go
9+
through: all, only TFVC branches and release stages ('-'), or repositories / text.
10+
- Installer, environments page: found rows start empty with the hint next to them ("Main [DEV]"), a repository
11+
filter (all / TFVC branches and release stages / one repository) and "Use the hints for the rows shown". Rows hidden
12+
by the filter keep what was typed and are saved too.
13+
314
## 0.11.3 — 2026-10-27
415

516
- Finding the environments includes TFVC branches ($/Project/Trunk/Main, Release, SIT…) with a changeset in the last

‎packaging/inno/ado-lens.iss‎

Lines changed: 116 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ Filename: "{cm:ReadmeUrl}"; Description: "{cm:OpenReadme}"; Flags: postinstall s
8484
[CustomMessages]
8585
en.EnvTitle=Environments
8686
en.EnvSub=Where code goes before it reaches production
87-
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.
87+
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.
8888
en.EnvDetecting=Looking for release stages and branches...
8989
en.EnvNone=No release stages or long-lived branches were found. Claude will ask about environments when it needs them.
9090
en.EnvExisting=Environments are already set: %1 (%2).
@@ -93,6 +93,9 @@ en.EnvMine=Save for me only (this computer)
9393
en.EnvTeam=Save for the whole team (wiki page "ADO Lens" of the project)
9494
en.EnvSkip=Skip - Claude asks when needed
9595
en.EnvMore=... and %1 more: Start menu > ADO Lens - settings > Environments.
96+
en.EnvAll=(all)
97+
en.EnvNoRepo=(TFVC branches and release stages)
98+
en.EnvHints=Use the hints for the rows shown
9699
en.EnvFailed=Saving the environments did not work:%n%n%1
97100
en.ReadmeUrl=https://github.com/willowdev-code/ado-lens#readme
98101
en.IconSettings=ADO Lens - settings
@@ -101,7 +104,7 @@ en.IconSkills=ADO Lens - skills for Claude Desktop
101104
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.
102105
pl.EnvTitle=Środowiska
103106
pl.EnvSub=Gdzie trafia kod, zanim dojdzie na produkcję
104-
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ń.
107+
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ń.
105108
pl.EnvDetecting=Szukam etapów release'ów i gałęzi...
106109
pl.EnvNone=Nie znaleziono etapów release'ów ani gałęzi. Claude zapyta o środowiska, gdy będą potrzebne.
107110
pl.EnvExisting=Środowiska są już ustawione: %1 (%2).
@@ -110,6 +113,9 @@ pl.EnvMine=Zapisz tylko dla mnie (ten komputer)
110113
pl.EnvTeam=Zapisz dla całego zespołu (strona wiki "ADO Lens" w projekcie)
111114
pl.EnvSkip=Pomiń - Claude zapyta, gdy będzie trzeba
112115
pl.EnvMore=... i jeszcze %1: menu Start > ADO Lens - ustawienia > Środowiska.
116+
pl.EnvAll=(wszystkie)
117+
pl.EnvNoRepo=(gałęzie TFVC i etapy release'ów)
118+
pl.EnvHints=Użyj podpowiedzi dla widocznych
113119
pl.EnvFailed=Nie udało się zapisać środowisk:%n%n%1
114120
pl.ReadmeUrl=https://github.com/willowdev-code/ado-lens/blob/main/README.pl.md
115121
pl.IconSettings=ADO Lens - ustawienia
@@ -173,7 +179,10 @@ var
173179
HasConnection: Boolean;
174180
EnvPage: TWizardPage;
175181
EnvInfo, EnvLater: TNewStaticText;
176-
EnvKeys: TArrayOfString;
182+
EnvKeys, EnvGuess, EnvRepo: TArrayOfString;
183+
EnvFilter: TNewComboBox;
184+
EnvHints: TNewButton;
185+
EnvRowsTop: Integer;
177186
EnvLabels: array of TNewStaticText;
178187
EnvEdits: array of TNewEdit;
179188
EnvScope: TNewComboBox;
@@ -419,25 +428,94 @@ begin
419428
EnvMore.Visible := False;
420429
end;
421430

431+
{ Shows the rows that match the filter, as many as fit between the filter and the drop-down; the rest is counted.
432+
Hidden rows keep what was typed and are saved too. }
433+
procedure LayoutEnvRows;
434+
var
435+
I, Shown, Hidden, MaxRows: Integer;
436+
Match: Boolean;
437+
begin
438+
MaxRows := (EnvMore.Top - EnvRowsTop) div ScaleY(26);
439+
Shown := 0;
440+
Hidden := 0;
441+
for I := 0 to GetArrayLength(EnvKeys) - 1 do
442+
begin
443+
if EnvFilter.ItemIndex <= 0 then Match := True
444+
else if EnvFilter.ItemIndex = 1 then Match := EnvRepo[I] = ''
445+
else Match := CompareText(EnvRepo[I], EnvFilter.Items[EnvFilter.ItemIndex]) = 0;
446+
if Match and (Shown < MaxRows) then
447+
begin
448+
EnvLabels[I].Top := EnvRowsTop + Shown * ScaleY(26) + ScaleY(3);
449+
EnvEdits[I].Top := EnvRowsTop + Shown * ScaleY(26);
450+
EnvLabels[I].Visible := True;
451+
EnvEdits[I].Visible := True;
452+
Shown := Shown + 1;
453+
end
454+
else
455+
begin
456+
if Match then Hidden := Hidden + 1;
457+
EnvLabels[I].Visible := False;
458+
EnvEdits[I].Visible := False;
459+
end;
460+
end;
461+
EnvMore.Visible := Hidden > 0;
462+
if Hidden > 0 then EnvMore.Caption := FmtMessage(CustomMessage('EnvMore'), [IntToStr(Hidden)]);
463+
end;
464+
465+
procedure EnvFilterChange(Sender: TObject);
466+
begin
467+
LayoutEnvRows;
468+
end;
469+
470+
{ As in the settings window: a row found gets no name until given; the hint is shown next to it. }
471+
procedure EnvHintsClick(Sender: TObject);
472+
var
473+
I: Integer;
474+
begin
475+
for I := 0 to GetArrayLength(EnvKeys) - 1 do
476+
if EnvEdits[I].Visible and (Trim(EnvEdits[I].Text) = '') and (EnvGuess[I] <> '') then
477+
EnvEdits[I].Text := EnvGuess[I];
478+
end;
479+
422480
{ Fills the environment page from "adolens setup --detect": the map already in use, or one row per stage or branch
423-
found with a suggested name. }
481+
found, with its hint. }
424482
procedure DetectEnvironments;
425483
var
426-
Exe, OutFile, Line, Key, Guess, Caption: String;
484+
Exe, OutFile, Line, Key, Guess, Caption, Repo: String;
427485
Lines: TArrayOfString;
428-
I, P, N, Top, MaxRows, Skipped: Integer;
486+
I, P, N, Repos: Integer;
429487
Code: Integer;
430488
begin
431-
{ As many rows as fit between the text and the drop-down; the rest is named and can be set in the settings later. }
432-
MaxRows := (EnvMore.Top - (EnvInfo.Top + EnvInfo.Height + ScaleY(4))) div ScaleY(26);
433-
Skipped := 0;
434489
EnvDetected := True;
435490
Exe := ExpandConstant('{tmp}\adolens.exe');
436491
OutFile := ExpandConstant('{tmp}\adolens-envs.txt');
437492
DeleteFile(OutFile);
438493
Exec(Exe, 'setup --detect --out "' + OutFile + '"', '', SW_HIDE, ewWaitUntilTerminated, Code);
439494
if not LoadStringsFromFile(OutFile, Lines) then SetArrayLength(Lines, 0);
495+
496+
{ The filter and the hints button, under the text. }
497+
EnvFilter := TNewComboBox.Create(EnvPage);
498+
EnvFilter.Parent := EnvPage.Surface;
499+
EnvFilter.Style := csDropDownList;
500+
EnvFilter.Top := EnvInfo.Top + EnvInfo.Height + ScaleY(2);
501+
EnvFilter.Width := EnvPage.SurfaceWidth div 2 - ScaleX(8);
502+
EnvFilter.Items.Add(CustomMessage('EnvAll'));
503+
EnvFilter.Items.Add(CustomMessage('EnvNoRepo'));
504+
EnvFilter.OnChange := @EnvFilterChange;
505+
EnvFilter.Visible := False;
506+
EnvHints := TNewButton.Create(EnvPage);
507+
EnvHints.Parent := EnvPage.Surface;
508+
EnvHints.Caption := CustomMessage('EnvHints');
509+
EnvHints.Left := EnvPage.SurfaceWidth div 2;
510+
EnvHints.Top := EnvFilter.Top;
511+
EnvHints.Width := EnvPage.SurfaceWidth div 2;
512+
EnvHints.Height := EnvFilter.Height;
513+
EnvHints.OnClick := @EnvHintsClick;
514+
EnvHints.Visible := False;
515+
EnvRowsTop := EnvFilter.Top + EnvFilter.Height + ScaleY(6);
516+
440517
N := 0;
518+
Repos := 0;
441519
SetArrayLength(EnvKeys, 0);
442520
for I := 0 to GetArrayLength(Lines) - 1 do
443521
begin
@@ -449,9 +527,7 @@ begin
449527
EnvInfo.Caption := FmtMessage(CustomMessage('EnvExisting'), [Copy(Line, 1, P - 1), Copy(Line, P + 1, Length(Line))]);
450528
Exit;
451529
end;
452-
if (Copy(Line, 1, 4) = 'row' + #9) and (N >= MaxRows) then
453-
Skipped := Skipped + 1
454-
else if Copy(Line, 1, 4) = 'row' + #9 then
530+
if (Copy(Line, 1, 4) = 'row' + #9) and (N < 80) then
455531
begin
456532
Delete(Line, 1, 4);
457533
P := Pos(#9, Line);
@@ -465,30 +541,41 @@ begin
465541
Key := Line;
466542
Guess := '';
467543
end;
544+
{ "D365FO:staging" is the branch staging of the repository D365FO. }
545+
Repo := '';
546+
Caption := Key;
547+
P := Pos(':', Key);
548+
if (P > 1) and (Pos('/', Key) = 0) then
549+
begin
550+
Repo := Copy(Key, 1, P - 1);
551+
Caption := Copy(Key, P + 1, Length(Key)) + ' (' + Repo + ')';
552+
if EnvFilter.Items.IndexOf(Repo) < 0 then
553+
begin
554+
EnvFilter.Items.Add(Repo);
555+
Repos := Repos + 1;
556+
end;
557+
end;
558+
if Guess <> '' then Caption := Caption + ' [' + Guess + ']';
468559
N := N + 1;
469560
SetArrayLength(EnvKeys, N);
561+
SetArrayLength(EnvGuess, N);
562+
SetArrayLength(EnvRepo, N);
470563
SetArrayLength(EnvLabels, N);
471564
SetArrayLength(EnvEdits, N);
472565
EnvKeys[N - 1] := Key;
473-
Top := EnvInfo.Top + EnvInfo.Height + ScaleY(4) + (N - 1) * ScaleY(26);
566+
EnvGuess[N - 1] := Guess;
567+
EnvRepo[N - 1] := Repo;
474568
EnvLabels[N - 1] := TNewStaticText.Create(EnvPage);
475569
EnvLabels[N - 1].Parent := EnvPage.Surface;
476-
{ "D365FO:staging" is the branch staging of the repository D365FO. }
477-
Caption := Key;
478-
P := Pos(':', Key);
479-
if (P > 1) and (Pos('/', Key) = 0) then
480-
Caption := Copy(Key, P + 1, Length(Key)) + ' (' + Copy(Key, 1, P - 1) + ')';
481570
EnvLabels[N - 1].Caption := Caption;
482571
EnvLabels[N - 1].AutoSize := False;
483572
EnvLabels[N - 1].Height := ScaleY(18);
484-
EnvLabels[N - 1].Top := Top + ScaleY(3);
485573
EnvLabels[N - 1].Width := EnvPage.SurfaceWidth div 2 - ScaleX(8);
486574
EnvEdits[N - 1] := TNewEdit.Create(EnvPage);
487575
EnvEdits[N - 1].Parent := EnvPage.Surface;
488576
EnvEdits[N - 1].Left := EnvPage.SurfaceWidth div 2;
489-
EnvEdits[N - 1].Top := Top;
490577
EnvEdits[N - 1].Width := EnvPage.SurfaceWidth div 2;
491-
EnvEdits[N - 1].Text := Guess;
578+
EnvEdits[N - 1].Text := '';
492579
end;
493580
end;
494581
if N = 0 then
@@ -497,11 +584,14 @@ begin
497584
Exit;
498585
end;
499586
EnvInfo.Caption := CustomMessage('EnvText');
500-
if Skipped > 0 then
501-
begin
502-
EnvMore.Caption := FmtMessage(CustomMessage('EnvMore'), [IntToStr(Skipped)]);
503-
EnvMore.Visible := True;
504-
end;
587+
{ With several repositories, start with the TFVC branches and release stages when there are any. }
588+
EnvFilter.ItemIndex := 0;
589+
if Repos > 0 then
590+
for I := 0 to N - 1 do
591+
if EnvRepo[I] = '' then EnvFilter.ItemIndex := 1;
592+
EnvFilter.Visible := True;
593+
EnvHints.Visible := True;
594+
LayoutEnvRows;
505595
EnvScope.Visible := True;
506596
end;
507597

‎src/AdoLens/AdoLens.csproj‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
<AssemblyTitle>ADO Lens for Azure DevOps</AssemblyTitle>
2626
<Description>Azure DevOps (work items, boards, wiki, Git and TFVC, pipelines) served to AI assistants over MCP</Description>
2727
<Copyright>Copyright © 2026 WillowDev</Copyright>
28-
<Version>0.11.3</Version>
28+
<Version>0.11.4</Version>
2929
</PropertyGroup>
3030

3131
<ItemGroup>

‎src/AdoLens/Cli.cs‎

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -678,27 +678,49 @@ async Task EnvironmentsAsync(string name, Session session, string project, bool
678678
Console.WriteLine(Ui.T($"{name} / {project}: code goes {string.Join(" -> ", effective.Values)} ({source}).",
679679
$"{name} / {project}: kod trafia kolejno na {string.Join(" -> ", effective.Values)} ({source})."));
680680
if (effective.Count > 0 && !change) return;
681-
var detected = await EnvironmentFlow.DetectAsync(session, project, default);
682-
// Rows: the current map in its order, then what was found and is not in it yet.
683-
var rows = effective.Select(e => (e.Key, Guess: (string?)e.Value))
684-
.Concat(detected.Where(d => !effective.ContainsKey(d.Key))).ToList();
685-
if (rows.Count == 0 || !interactive)
681+
var found = (await EnvironmentFlow.DetectAsync(session, project, default)).Where(d => !effective.ContainsKey(d.Key)).ToList();
682+
if ((effective.Count == 0 && found.Count == 0) || !interactive)
686683
{
687684
Console.WriteLine(Ui.T($"{name} / {project}: no release stages or long-lived branches found - Claude will ask when needed.",
688685
$"{name} / {project}: nie znaleziono etapów release'ów ani gałęzi - Claude zapyta, gdy będzie trzeba."));
689686
return;
690687
}
691688
Console.WriteLine(Ui.T($"{name} / {project}: where does code go before it reaches production?",
692689
$"{name} / {project}: przez jakie środowiska przechodzi kod, zanim trafi na produkcję?"));
693-
Console.WriteLine(Ui.T("Name each as people say it (e.g. UAT (sbx03)); Enter keeps [the name], '-' leaves it out.",
694-
"Nadaj każdemu nazwę, której używacie (np. UAT (sbx03)); Enter zostawia [nazwę], '-' pomija."));
695690
var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
696-
foreach (var (key, guess) in rows)
691+
// 1. The map in use: Enter keeps a name, '-' removes it.
692+
if (effective.Count > 0)
693+
Console.WriteLine(Ui.T("In use - Enter keeps [the name], '-' removes it:", "W użyciu - Enter zostawia [nazwę], '-' usuwa:"));
694+
foreach (var (key, current) in effective)
697695
{
698-
var label = Prompt($" {key}{(guess is null ? "" : $" [{guess}]")}: ");
696+
var label = Prompt($" {key} [{current}]: ");
699697
if (label == "-") continue;
700-
var value = string.IsNullOrWhiteSpace(label) ? guess : label.Trim();
701-
if (value is not null) map[key] = value;
698+
map[key] = string.IsNullOrWhiteSpace(label) ? current : label.Trim();
699+
}
700+
// 2. What was found, as in the settings window: no name until given, so a project with many repositories does
701+
// not save every branch. Many rows: narrow them down first.
702+
if (found.Count > 8)
703+
{
704+
var repos = found.Select(f => EnvMap.RepoOf(f.Key)).OfType<string>().Distinct(StringComparer.OrdinalIgnoreCase).Order(StringComparer.OrdinalIgnoreCase).ToList();
705+
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 ? ", …" : "")}).",
706+
$"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 ? ", …" : "")})."));
707+
var only = Prompt(Ui.T(" go through: Enter = all, '-' = only TFVC branches and release stages, or repositories / text (e.g. D365FO): ",
708+
" które przejrzeć: Enter = wszystkie, '-' = tylko gałęzie TFVC i etapy, albo repozytoria / tekst (np. D365FO): "))?.Trim();
709+
if (only == "-") found = found.Where(f => EnvMap.RepoOf(f.Key) is null).ToList();
710+
else if (!string.IsNullOrEmpty(only))
711+
{
712+
var words = only.Split([',', ';', ' '], StringSplitOptions.RemoveEmptyEntries);
713+
found = found.Where(f => words.Any(w => f.Key.Contains(w, StringComparison.OrdinalIgnoreCase))).ToList();
714+
}
715+
}
716+
if (found.Count > 0)
717+
Console.WriteLine(Ui.T("Found - type the name as people say it (e.g. UAT (sbx03)), '+' takes the hint, Enter skips:",
718+
"Znalezione - wpisz nazwę, jakiej używacie (np. UAT (sbx03)), '+' przyjmuje podpowiedź, Enter pomija:"));
719+
foreach (var (key, hint) in found)
720+
{
721+
var label = Prompt($" {key}{(hint is null ? "" : Ui.T($" (looks like {hint})", $" (wygląda na {hint})"))}: ")?.Trim();
722+
if (label == "+" && hint is not null) map[key] = hint;
723+
else if (!string.IsNullOrEmpty(label) && label != "+" && label != "-") map[key] = label;
702724
}
703725
// Something the search did not find (a release stage that has not run yet, a branch in another project).
704726
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)

0 commit comments

Comments
 (0)