summaryrefslogtreecommitdiff
path: root/makima/frontend/src/routes
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-03-04 16:47:12 +0000
committerGitHub <noreply@github.com>2026-03-04 16:47:12 +0000
commitec9738a069e61529be040eff065318972b8a11e2 (patch)
treed1b15d3b22d4980acff4fba8a12b99920035025c /makima/frontend/src/routes
parent78cb861412850889424ae7d5ae5cd952a2b90295 (diff)
downloadsoryu-ec9738a069e61529be040eff065318972b8a11e2.tar.gz
soryu-ec9738a069e61529be040eff065318972b8a11e2.zip
feat: task slide-out panel, 3-way reconcile toggle, daemon reauth fix (#85)
* WIP: heartbeat checkpoint * WIP: heartbeat checkpoint * feat: soryu-co/soryu - makima: Fix daemon reauth flow for new claude setup-token output format * feat: soryu-co/soryu - makima: Update frontend reconcile toggle to three-way switch * feat: soryu-co/soryu - makima: Add task slide-out panel to directive page
Diffstat (limited to 'makima/frontend/src/routes')
-rw-r--r--makima/frontend/src/routes/daemons.tsx261
1 files changed, 95 insertions, 166 deletions
diff --git a/makima/frontend/src/routes/daemons.tsx b/makima/frontend/src/routes/daemons.tsx
index ca167fe..aa48deb 100644
--- a/makima/frontend/src/routes/daemons.tsx
+++ b/makima/frontend/src/routes/daemons.tsx
@@ -6,7 +6,6 @@ import {
listDaemons,
restartDaemon,
triggerDaemonReauth,
- submitDaemonAuthCode,
getDaemonReauthStatus,
type Daemon,
type DaemonListResponse,
@@ -43,7 +42,7 @@ function ErrorAlert({ children }: { children: React.ReactNode }) {
type ReauthState =
| { phase: "initiating" }
| { phase: "url_ready"; loginUrl: string; requestId: string }
- | { phase: "submitting"; requestId: string }
+ | { phase: "waiting_for_auth"; loginUrl: string; requestId: string }
| { phase: "success" }
| { phase: "error"; message: string };
@@ -55,7 +54,6 @@ function ReauthModal({
onClose: () => void;
}) {
const [state, setState] = useState<ReauthState>({ phase: "initiating" });
- const [authCode, setAuthCode] = useState("");
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
// Cleanup polling on unmount
@@ -67,6 +65,53 @@ function ReauthModal({
};
}, []);
+ // Start polling for status updates (URL ready, then completion)
+ const startPolling = useCallback(
+ (requestId: string) => {
+ if (pollingRef.current) {
+ clearInterval(pollingRef.current);
+ }
+ pollingRef.current = setInterval(async () => {
+ try {
+ const status = await getDaemonReauthStatus(daemon.id, requestId);
+
+ if (status.status === "completed") {
+ setState({ phase: "success" });
+ if (pollingRef.current) {
+ clearInterval(pollingRef.current);
+ pollingRef.current = null;
+ }
+ } else if (status.status === "url_ready" && status.loginUrl) {
+ setState((prev) => {
+ // Only update if we haven't already shown the URL
+ if (prev.phase === "initiating") {
+ return {
+ phase: "url_ready",
+ loginUrl: status.loginUrl!,
+ requestId,
+ };
+ }
+ return prev;
+ });
+ // Keep polling for completion - don't stop here
+ } else if (status.status === "failed") {
+ setState({
+ phase: "error",
+ message: status.error || "Reauth failed",
+ });
+ if (pollingRef.current) {
+ clearInterval(pollingRef.current);
+ pollingRef.current = null;
+ }
+ }
+ } catch {
+ // Polling errors are non-fatal, keep trying
+ }
+ }, 2000);
+ },
+ [daemon.id],
+ );
+
// Trigger reauth on mount
useEffect(() => {
let cancelled = false;
@@ -74,45 +119,7 @@ function ReauthModal({
try {
const res = await triggerDaemonReauth(daemon.id);
if (cancelled) return;
-
- // Start polling for status
- const requestId = res.requestId;
- pollingRef.current = setInterval(async () => {
- try {
- const status = await getDaemonReauthStatus(daemon.id, requestId);
- if (cancelled) return;
-
- if (status.status === "url_ready" && status.loginUrl) {
- setState({
- phase: "url_ready",
- loginUrl: status.loginUrl,
- requestId,
- });
- // Stop polling once we have the URL
- if (pollingRef.current) {
- clearInterval(pollingRef.current);
- pollingRef.current = null;
- }
- } else if (status.status === "failed") {
- setState({
- phase: "error",
- message: status.error || "Reauth failed",
- });
- if (pollingRef.current) {
- clearInterval(pollingRef.current);
- pollingRef.current = null;
- }
- } else if (status.status === "completed") {
- setState({ phase: "success" });
- if (pollingRef.current) {
- clearInterval(pollingRef.current);
- pollingRef.current = null;
- }
- }
- } catch {
- // Polling errors are non-fatal, keep trying
- }
- }, 2000);
+ startPolling(res.requestId);
} catch (err) {
if (cancelled) return;
setState({
@@ -126,110 +133,28 @@ function ReauthModal({
return () => {
cancelled = true;
};
- }, [daemon.id]);
-
- const handleSubmitCode = useCallback(
- async (e: React.FormEvent) => {
- e.preventDefault();
- if (!authCode.trim() || state.phase !== "url_ready") return;
-
- const requestId = state.requestId;
- setState({ phase: "submitting", requestId });
-
- try {
- await submitDaemonAuthCode(daemon.id, authCode.trim(), requestId);
-
- // Poll for completion
- pollingRef.current = setInterval(async () => {
- try {
- const status = await getDaemonReauthStatus(
- daemon.id,
- requestId,
- );
- if (status.status === "completed") {
- setState({ phase: "success" });
- if (pollingRef.current) {
- clearInterval(pollingRef.current);
- pollingRef.current = null;
- }
- } else if (status.status === "failed") {
- setState({
- phase: "error",
- message: status.error || "Auth code submission failed",
- });
- if (pollingRef.current) {
- clearInterval(pollingRef.current);
- pollingRef.current = null;
- }
- }
- } catch {
- // Keep polling
- }
- }, 2000);
-
- // Also set a timeout so we don't poll forever
- setTimeout(() => {
- if (pollingRef.current) {
- clearInterval(pollingRef.current);
- pollingRef.current = null;
- }
- // If still submitting after 30s, assume success (setup-token completed)
- setState((prev) =>
- prev.phase === "submitting" ? { phase: "success" } : prev,
- );
- }, 30000);
- } catch (err) {
- setState({
- phase: "error",
- message:
- err instanceof Error
- ? err.message
- : "Failed to submit auth code",
- });
+ }, [daemon.id, startPolling]);
+
+ // When URL is shown, transition to waiting_for_auth after user clicks the link
+ const handleOpenedLink = useCallback(() => {
+ setState((prev) => {
+ if (prev.phase === "url_ready") {
+ return {
+ phase: "waiting_for_auth",
+ loginUrl: prev.loginUrl,
+ requestId: prev.requestId,
+ };
}
- },
- [authCode, daemon.id, state],
- );
+ return prev;
+ });
+ }, []);
const handleRetry = useCallback(() => {
- setAuthCode("");
setState({ phase: "initiating" });
- // Re-trigger will happen via the useEffect dependency change
- // We need to manually trigger since daemon.id hasn't changed
const trigger = async () => {
try {
const res = await triggerDaemonReauth(daemon.id);
- const requestId = res.requestId;
- pollingRef.current = setInterval(async () => {
- try {
- const status = await getDaemonReauthStatus(
- daemon.id,
- requestId,
- );
- if (status.status === "url_ready" && status.loginUrl) {
- setState({
- phase: "url_ready",
- loginUrl: status.loginUrl,
- requestId,
- });
- if (pollingRef.current) {
- clearInterval(pollingRef.current);
- pollingRef.current = null;
- }
- } else if (status.status === "failed") {
- setState({
- phase: "error",
- message: status.error || "Reauth failed",
- });
- if (pollingRef.current) {
- clearInterval(pollingRef.current);
- pollingRef.current = null;
- }
- }
- } catch {
- // Keep polling
- }
- }, 2000);
+ startPolling(res.requestId);
} catch (err) {
setState({
phase: "error",
@@ -241,7 +166,7 @@ function ReauthModal({
}
};
trigger();
- }, [daemon.id]);
+ }, [daemon.id, startPolling]);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
@@ -272,46 +197,50 @@ function ReauthModal({
</div>
)}
- {/* URL Ready */}
+ {/* URL Ready - user needs to click the link */}
{state.phase === "url_ready" && (
<div className="space-y-3">
<p className="text-[10px] font-mono text-[#7788aa]">
- Click the button below to open the OAuth login page, then paste the code:
+ Click the button below to open the OAuth login page. Authentication will complete automatically.
</p>
<a
href={state.loginUrl}
target="_blank"
rel="noopener noreferrer"
+ onClick={handleOpenedLink}
className="block text-center bg-amber-500 hover:bg-amber-400 text-black font-mono text-xs font-medium px-4 py-2 transition-colors"
>
- 1. Login to Claude
+ Login to Claude
</a>
- <form onSubmit={handleSubmitCode} className="flex gap-2">
- <input
- type="text"
- value={authCode}
- onChange={(e) => setAuthCode(e.target.value)}
- placeholder="2. Paste authentication code"
- className="flex-1 bg-[#0a1525] border border-amber-500/30 px-3 py-2 text-xs font-mono text-amber-100 placeholder-amber-500/50 focus:outline-none focus:border-amber-400"
- />
- <button
- type="submit"
- disabled={!authCode.trim()}
- className="bg-amber-500 hover:bg-amber-400 disabled:bg-amber-700 disabled:cursor-not-allowed text-black font-mono text-xs font-medium px-4 py-2 transition-colors"
- >
- Submit
- </button>
- </form>
+ <div className="flex items-center gap-2 pt-1">
+ <div className="w-2 h-2 bg-amber-500/50 rounded-full animate-pulse" />
+ <span className="text-[10px] font-mono text-[#7788aa]">
+ Waiting for authentication to complete...
+ </span>
+ </div>
</div>
)}
- {/* Submitting */}
- {state.phase === "submitting" && (
- <div className="flex items-center gap-2 py-4">
- <div className="w-3 h-3 border border-amber-400 border-t-transparent rounded-full animate-spin" />
- <span className="text-[10px] font-mono text-[#7788aa]">
- Submitting auth code...
- </span>
+ {/* Waiting for auth - user has clicked the link, waiting for token */}
+ {state.phase === "waiting_for_auth" && (
+ <div className="space-y-3">
+ <div className="flex items-center gap-2 py-2">
+ <div className="w-3 h-3 border border-amber-400 border-t-transparent rounded-full animate-spin" />
+ <span className="text-[10px] font-mono text-[#7788aa]">
+ Waiting for authentication to complete...
+ </span>
+ </div>
+ <p className="text-[10px] font-mono text-[#556677]">
+ Complete the login in your browser. The token will be saved automatically.
+ </p>
+ <a
+ href={state.loginUrl}
+ target="_blank"
+ rel="noopener noreferrer"
+ className="inline-block text-[10px] font-mono text-amber-500/70 hover:text-amber-400 underline"
+ >
+ Open login page again
+ </a>
</div>
)}