fix(claude-code): /claude-code-login reports the outcome of a login it joined

This commit is contained in:
EugeneTes
2026-09-25 17:54:20 +00:00
parent f620b33771
commit bbece220c8
2 changed files with 45 additions and 5 deletions

View File

@@ -8,7 +8,8 @@ namespace Volcanic.AgenticCore.Extensions.ClaudeCode;
/// <c>/claude-code-login</c>: signs the <c>claude</c> CLI in (again) through the current session's own UI, with no
/// owner redirect - whoever runs the command is already allow-listed and at the keyboard. Also useful to switch
/// accounts or refresh a session that hasn't expired yet. The outcome is reported by <see cref="ClaudeCodeApi.LoginThroughAsync"/>
/// through that same UI.
/// through that same UI; when the command joins a login already running elsewhere (whose UI is another chat), it
/// reports that login's outcome here itself.
/// </summary>
public static class LoginCommand
{
@@ -29,9 +30,17 @@ public static class LoginCommand
return;
}
var ui = ctx.Ui;
await (claude.Options.Coordinator ?? ClaudeLoginCoordinator.Shared)
.EnsureLoggedInAsync(flowCt => claude.RunLoginFlowAsync(_ => Task.FromResult<IUserInterface?>(ui), flowCt), ct)
var drivenHere = false;
var outcome = await (claude.Options.Coordinator ?? ClaudeLoginCoordinator.Shared)
.EnsureLoggedInAsync(flowCt =>
{
drivenHere = true;
return claude.RunLoginFlowAsync(_ => Task.FromResult<IUserInterface?>(ui), flowCt);
}, ct)
.ConfigureAwait(false);
if (drivenHere) return;
if (outcome.Success) ui.Notify("A Claude Code login was already in progress elsewhere; it succeeded ✓");
else ui.Notify($"A Claude Code login was already in progress elsewhere; it failed: {outcome.Error}", NotifyKind.Error);
});
}
}

View File

@@ -106,10 +106,10 @@ public class LoginCommandTests : IDisposable
return script;
}
private ClaudeCodeApi Claude(string claudePath) => new(new SessionRegistry(), new ClaudeCodeApiOptions
private ClaudeCodeApi Claude(string claudePath, ClaudeLoginCoordinator? coordinator = null) => new(new SessionRegistry(), new ClaudeCodeApiOptions
{
ClaudePath = claudePath,
Coordinator = new ClaudeLoginCoordinator(),
Coordinator = coordinator ?? new ClaudeLoginCoordinator(),
GetEnv = _ => null,
ParentEnvironment = new Dictionary<string, string?> { ["PATH"] = Environment.GetEnvironmentVariable("PATH") },
});
@@ -142,4 +142,35 @@ public class LoginCommandTests : IDisposable
Assert.Contains(ui.Notes, n => n.Message.Contains("login complete"));
Assert.Empty(api.Sent);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task Joining_a_login_driven_elsewhere_reports_its_outcome_here(bool success)
{
var coordinator = new ClaudeLoginCoordinator();
var gate = new TaskCompletionSource<LoginOutcome>(TaskCreationOptions.RunContinuationsAsynchronously);
var elsewhere = coordinator.EnsureLoggedInAsync(_ => gate.Task, CancellationToken.None);
var command = LoginCommand.Create(new FakeExtensionApi(), Claude("/nonexistent/claude", coordinator));
var ui = new RecordingUi("never-asked");
var running = command.Handler("", new FakeContext(ui), CancellationToken.None);
gate.SetResult(success ? LoginOutcome.Ok : LoginOutcome.Failed("no code was entered"));
await running.WaitAsync(TimeSpan.FromSeconds(10));
await elsewhere;
Assert.Empty(ui.Questions); // the other flow's UI drove the login
var (message, kind) = Assert.Single(ui.Notes);
Assert.Contains("already in progress elsewhere", message);
if (success)
{
Assert.Contains("succeeded", message);
Assert.Equal(NotifyKind.Info, kind);
}
else
{
Assert.Contains("failed: no code was entered", message);
Assert.Equal(NotifyKind.Error, kind);
}
}
}