mara: "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 faithful port of app.js's el().append()-per-line but not what a hand-authored component should do. Wraps each state's content in one .login-card div instead, styled to match Dropdown's .ui-dropdown (@hive/shared) background/border/radius/shadow values exactly, so this reads as the same popup family as every other floating surface in the app rather than a one-off. Not the Dropdown component itself — its options.map render is menu-item-shaped, not a fit for a login form + output pane. agent.css's .agent-status-overlay > * rule is untouched (app.js's still-live markup depends on it verbatim until the index.html cutover) — .login-card coexists with it rather than editing it, correctly overriding on equal specificity since main.css loads after agent.css.
171 lines
5.7 KiB
TypeScript
171 lines
5.7 KiB
TypeScript
// <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>
|
|
▶{' '}
|
|
<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>
|
|
);
|
|
}
|