Watch
0
0
Fork
You've already forked hyperhive
0

hive-agent: the agent has no login flow of its own, credentials come from the swarm

This commit is contained in:
damocles 2026-11-06 23:17:11 +01:00 • committed by mara
commit ac3b3cfcd3
4 changed files with 46 additions and 844 deletions

View file

@ -1,36 +0,0 @@
/* .login-card — one elevated-surface card for the whole login-recovery
overlay, replacing the old markup's per-`<p>`/`<form>` boxing
(`agent.css`'s `.agent-status-overlay > *`, still needed there
verbatim since app.js's still-live markup depends on it until the
index.html cutover — this file coexists with it rather than editing
it). Same visual language as `Dropdown`'s `.ui-dropdown`
(`@hive/shared/dropdown/Dropdown.css`) — background/border/radius/
shadow values match exactly, so this reads as the same "popup" family
as every other floating surface in the app, just laid out as a static
block instead of an anchored, position:absolute menu (that part of
`Dropdown` is specific to hanging under a trigger and doesn't apply
here — `.agent-status-overlay` already centres this card itself). */
.login-card {
display: flex;
flex-direction: column;
gap: 0.6em;
background: var(--bg-elev);
border: 1px solid var(--border);
border-radius: 0.5em;
box-shadow: 0 0.4em 1em rgba(0, 0, 0, 0.35);
padding: 1em 1.2em;
}
.login-card > * {
margin: 0;
}
/* claude's real OAuth URL is one long unbroken token (a query string
with no whitespace) — without this it doesn't wrap at the card's
bounded width (.agent-status-overlay caps at 44em) and instead
overflows straight past the card's right edge (mara reported this
with a screenshot). `overflow-wrap: anywhere` lets the
browser break mid-token as a last resort, same as any other
long-URL-in-a-narrow-column case — `word-break: break-all` is the
older alternative but breaks more eagerly than needed here. */
.login-url {
overflow-wrap: anywhere;
}

View file

@ -1,186 +0,0 @@
// <LoginFlow> — the `needs_login_idle` / `needs_login_in_progress`
// overlay. Ports app.js's `renderNeedsLoginIdle`/`renderLoginInProgress`
// as real functional UI, not just a status label: an agent whose
// credentials got rotated (or hit `/logout`) has NO other way back
// online, so this is load-bearing, not cosmetic. Reuses agent.css's
// `.status-needs-login`/`.btn-login`/`.loginform`/`.loginform-reveal`
// rules verbatim (same class names as the old markup).
//
// mara (screenshot review): "reuse the popup component for the login,
// the floating separate panels should be one card." The old markup's
// `.agent-status-overlay > *` rule boxed every direct-child `<p>`/
// `<form>` separately (a fine translation of `el().append()`-per-line,
// but not what a hand-authored component should do) — this renders one
// `.login-card` instead, styled with the same "elevated surface" look
// `Dropdown`'s `.ui-dropdown` already established (`@hive/shared/
// dropdown/Dropdown.css`), not `Dropdown` itself (its `options.map`
// render is menu-item-shaped, not a generic content container — not a
// fit for a login form + output pane).
import { useState } from "preact/hooks";
import type { SessionView } from "../types.js";
import {
postLoginStart,
postLoginCode,
postLoginCancel,
} from "../lib/loginAction.js";
import "./LoginFlow.css";
function LoginIdle({ onStarted }: { onStarted: () => void }) {
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
function submit(e: Event) {
e.preventDefault();
setBusy(true);
setError(null);
postLoginStart().then((r) => {
setBusy(false);
if (r.ok) onStarted();
else setError(r.detail ?? "failed");
});
}
return (
<div class="login-card">
<p class="status-needs-login">◌ NEEDS L0G1N</p>
<p>
No Claude session in <code>~/.claude/</code>. The harness is up but the
turn loop is paused until you log in.
</p>
<form onSubmit={submit}>
<button type="submit" class="btn btn-login" disabled={busy}>
◆ ST4RT L0G1N
</button>
</form>
{error ? <p class="meta">error: {error}</p> : null}
<p class="meta">
Spawns <code>claude auth login</code> over plain stdio pipes. The OAuth
URL will appear here when claude emits it; paste the resulting code back
into the form below.
</p>
</div>
);
}
function LoginInProgress({
session,
onDone,
}: {
session: SessionView | null;
onDone: () => void;
}) {
const [code, setCode] = useState("");
const [reveal, setReveal] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const finished = session?.finished ?? false;
function submitCode(e: Event) {
e.preventDefault();
if (!code) return;
setBusy(true);
setError(null);
// Success: the next `/api/state` poll flips `status` back to
// `online` (or leaves it `needs_login_in_progress` on a bad code,
// with the process's own stderr showing up in `output` below) —
// nothing local to update here either way.
postLoginCode(code).then((r) => {
setBusy(false);
if (!r.ok) setError(r.detail ?? "failed");
});
}
function cancel(e: Event) {
e.preventDefault();
setBusy(true);
postLoginCancel().then(() => {
setBusy(false);
onDone();
});
}
return (
<div class="login-card">
<p class="status-needs-login">◌ L0G1N 1N PR0GRESS</p>
{session?.url ? (
<>
<p class="login-url">
▶{" "}
<a href={session.url} target="_blank" rel="noreferrer">
{session.url}
</a>
</p>
<p class="meta">
open this URL in a browser, complete the OAuth flow, paste the
resulting code below.
</p>
</>
) : (
<p class="meta">
waiting for claude to emit an OAuth URL on stdout… (output below)
</p>
)}
{!finished ? (
<form class="loginform" onSubmit={submitCode}>
<input
name="code"
type={reveal ? "text" : "password"}
placeholder="paste OAuth code here (hidden)"
required
autocomplete="one-time-code"
spellcheck={false}
value={code}
onInput={(e) => setCode((e.target as HTMLInputElement).value)}
/>
<button
type="button"
class="loginform-reveal"
title="show / hide pasted code"
aria-label="show / hide pasted OAuth code"
aria-pressed={reveal}
onClick={() => setReveal((r) => !r)}
>
👁
</button>
<button type="submit" class="btn btn-login" disabled={busy}>
◆ S3ND C0DE
</button>
</form>
) : null}
<form style={{ marginTop: "0.4em" }} onSubmit={cancel}>
<button type="submit" class="btn btn-cancel" disabled={busy}>
cancel + kill
</button>
</form>
{finished ? (
<p class="status-needs-login">
claude process exited: {session?.exit_note || "exited"}. Start over if
needed.
</p>
) : null}
{error ? <p class="meta">error: {error}</p> : null}
<h3>output</h3>
<pre class="diff">{session?.output || ""}</pre>
</div>
);
}
export function LoginFlow({
status,
session,
onRefresh,
}: {
status: "needs_login_idle" | "needs_login_in_progress";
session: SessionView | null;
onRefresh: () => void;
}) {
return (
<div class="agent-status-overlay">
{status === "needs_login_idle" ? (
<LoginIdle onStarted={onRefresh} />
) : (
<LoginInProgress session={session} onDone={onRefresh} />
)}
</div>
);
}

View file

@ -1,7 +1,6 @@
/* <MetaNav> popover — matches `@hive/shared`'s `Dropdown` (`.ui-dropdown`)
values exactly (background/border/radius/shadow, item padding/hover),
same "reuse the values, not the component" call `.login-card`
(LoginFlow.css) already made, for the same reason: this popover's
a "reuse the values, not the component" call: this popover's
items are real `<a>` tags, not `Dropdown`'s command-dispatch
`<button>`s. Right-anchored — MetaNav sits in the header's right-hand
pills cluster, so a left anchor would push it off-screen. */

View file

@ -19,8 +19,7 @@
// always `<button>`s for command dispatch, which would lose real link
// semantics like ctrl/middle-click and "copy link address"), but
// matching its `.ui-dropdown` visual values exactly (see MetaNav.css) —
// the same "reuse the values, not the component" call `LoginFlow`'s
// `.login-card` makes for the same reason.
// a "reuse the values, not the component" call.
import { useEffect, useRef, useState } from "preact/hooks";
import { Badge } from "@hive/shared/badge.js";
import { LinkIcon } from "@hive/shared/icons.js";