Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2f17799fc | ||
|
|
41cc2f5e8e |
3 changed files with 64 additions and 1 deletions
|
|
@ -2,7 +2,8 @@
|
|||
// login minter. Opening it starts the login (`POST …/claude-login`), which
|
||||
// answers with the URL claude printed; the operator opens it and pastes the
|
||||
// code back (`POST …/claude-login/code`). Closing it while the code is
|
||||
// still awaited stops the login (`DELETE …/claude-login`).
|
||||
// still awaited stops the login (`DELETE …/claude-login`). Closing while the
|
||||
// start is still in flight stops it once the start answers.
|
||||
//
|
||||
// Mounted only while open, so each open is a fresh login.
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
|
@ -15,9 +16,12 @@ import { TextField } from "../../ui/text-field/TextField.js";
|
|||
import {
|
||||
afterCode,
|
||||
afterStart,
|
||||
beginStart,
|
||||
claudeLoginPath,
|
||||
isLatestStart,
|
||||
leavesLoginRunning,
|
||||
pastedCode,
|
||||
startOutlivedDialog,
|
||||
type LoginStep,
|
||||
} from "./ClaudeLoginSteps.js";
|
||||
import "./ClaudeLoginDialog.css";
|
||||
|
|
@ -38,10 +42,15 @@ export function ClaudeLoginDialog({
|
|||
|
||||
useEffect(() => {
|
||||
let live = true;
|
||||
const mine = beginStart(agent);
|
||||
void (async () => {
|
||||
try {
|
||||
const r = await fetch(claudeLoginPath(agent), { method: "POST" });
|
||||
const problem = await problemOf(r);
|
||||
if (startOutlivedDialog(live, problem, isLatestStart(agent, mine))) {
|
||||
void fetch(claudeLoginPath(agent), { method: "DELETE" });
|
||||
return;
|
||||
}
|
||||
const next = afterStart(
|
||||
problem ? { problem } : ((await r.json()) as { url: string }),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -34,6 +34,35 @@ export function leavesLoginRunning(s: LoginStep): boolean {
|
|||
return s.step === "code";
|
||||
}
|
||||
|
||||
// The newest start per agent. The minter keeps one login per agent and a
|
||||
// new start replaces it, so a late stop from an older start would end the
|
||||
// newer dialog's login.
|
||||
const latestStart = new Map<string, number>();
|
||||
|
||||
// Marks a start as the agent's newest and returns its number.
|
||||
export function beginStart(agent: string): number {
|
||||
const n = (latestStart.get(agent) ?? 0) + 1;
|
||||
latestStart.set(agent, n);
|
||||
return n;
|
||||
}
|
||||
|
||||
// Whether `n` is still the agent's newest start.
|
||||
export function isLatestStart(agent: string, n: number): boolean {
|
||||
return latestStart.get(agent) === n;
|
||||
}
|
||||
|
||||
// Whether a start that answered after the dialog went away left a login
|
||||
// running that nothing else owns: it did unless the start itself failed or a
|
||||
// newer start has replaced it. The dialog stops it then, since nothing is
|
||||
// left to.
|
||||
export function startOutlivedDialog(
|
||||
dialogLive: boolean,
|
||||
problem: ProblemDetails | null,
|
||||
latest: boolean,
|
||||
): boolean {
|
||||
return !dialogLive && problem === null && latest;
|
||||
}
|
||||
|
||||
// What the code form may submit: the pasted code without surrounding
|
||||
// whitespace, or nothing.
|
||||
export function pastedCode(raw: string): string | null {
|
||||
|
|
|
|||
|
|
@ -6,9 +6,12 @@ import { test } from "node:test";
|
|||
import {
|
||||
afterCode,
|
||||
afterStart,
|
||||
beginStart,
|
||||
claudeLoginPath,
|
||||
isLatestStart,
|
||||
leavesLoginRunning,
|
||||
pastedCode,
|
||||
startOutlivedDialog,
|
||||
} from "../src/agent/claude-login/ClaudeLoginSteps.ts";
|
||||
|
||||
test("the routes name the agent, escaped", () => {
|
||||
|
|
@ -48,6 +51,28 @@ test("only a login waiting for its code is stopped on close", () => {
|
|||
assert.equal(leavesLoginRunning({ step: "stored" }), false);
|
||||
});
|
||||
|
||||
test("a start that answers after the dialog closed is stopped, unless it failed or was replaced", () => {
|
||||
assert.equal(startOutlivedDialog(false, null, true), true);
|
||||
assert.equal(startOutlivedDialog(true, null, true), false);
|
||||
assert.equal(
|
||||
startOutlivedDialog(false, { detail: "no minter" }, true),
|
||||
false,
|
||||
);
|
||||
assert.equal(startOutlivedDialog(false, null, false), false);
|
||||
});
|
||||
|
||||
test("only an agent's newest start counts as the latest", () => {
|
||||
const first = beginStart("login-test-a");
|
||||
assert.equal(isLatestStart("login-test-a", first), true);
|
||||
const second = beginStart("login-test-a");
|
||||
assert.equal(isLatestStart("login-test-a", first), false);
|
||||
assert.equal(isLatestStart("login-test-a", second), true);
|
||||
// Another agent's starts are counted on their own.
|
||||
const other = beginStart("login-test-b");
|
||||
assert.equal(isLatestStart("login-test-a", second), true);
|
||||
assert.equal(isLatestStart("login-test-b", other), true);
|
||||
});
|
||||
|
||||
test("a pasted code is trimmed, and a blank one is nothing", () => {
|
||||
assert.equal(pastedCode(" abc-def \n"), "abc-def");
|
||||
assert.equal(pastedCode(" \n "), null);
|
||||
|
|
|
|||
Loading…
Reference in a new issue