From 0e30f1790cd3a1717dcb55ae137700de9bb0dfcb Mon Sep 17 00:00:00 2001 From: soryu Date: Thu, 5 Mar 2026 16:05:01 +0000 Subject: fix: restore code entry in daemon reauth flow The previous commit removed the manual auth code input, expecting OAuth to complete automatically via token auto-detection. This restores the code entry field as the primary path while keeping token auto-detection as a parallel fallback. Co-Authored-By: Claude Opus 4.6 --- makima/frontend/src/routes/daemons.tsx | 134 ++++++++++++++++++++++----------- makima/frontend/tsconfig.tsbuildinfo | 2 +- makima/src/daemon/task/manager.rs | 88 ++++++++++++++++------ 3 files changed, 156 insertions(+), 68 deletions(-) (limited to 'makima') diff --git a/makima/frontend/src/routes/daemons.tsx b/makima/frontend/src/routes/daemons.tsx index aa48deb..a13c6de 100644 --- a/makima/frontend/src/routes/daemons.tsx +++ b/makima/frontend/src/routes/daemons.tsx @@ -6,6 +6,7 @@ import { listDaemons, restartDaemon, triggerDaemonReauth, + submitDaemonAuthCode, getDaemonReauthStatus, type Daemon, type DaemonListResponse, @@ -42,7 +43,7 @@ function ErrorAlert({ children }: { children: React.ReactNode }) { type ReauthState = | { phase: "initiating" } | { phase: "url_ready"; loginUrl: string; requestId: string } - | { phase: "waiting_for_auth"; loginUrl: string; requestId: string } + | { phase: "submitting"; requestId: string } | { phase: "success" } | { phase: "error"; message: string }; @@ -54,6 +55,7 @@ function ReauthModal({ onClose: () => void; }) { const [state, setState] = useState({ phase: "initiating" }); + const [authCode, setAuthCode] = useState(""); const pollingRef = useRef | null>(null); // Cleanup polling on unmount @@ -135,21 +137,71 @@ function ReauthModal({ }; }, [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, - }; + 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", + }); } - return prev; - }); - }, []); + }, + [authCode, daemon.id, state], + ); const handleRetry = useCallback(() => { + setAuthCode(""); setState({ phase: "initiating" }); const trigger = async () => { try { @@ -197,50 +249,46 @@ function ReauthModal({ )} - {/* URL Ready - user needs to click the link */} + {/* URL Ready */} {state.phase === "url_ready" && (

- Click the button below to open the OAuth login page. Authentication will complete automatically. + Click the button below to open the OAuth login page, then paste the code:

- Login to Claude + 1. Login to Claude -
-
- - Waiting for authentication to complete... - -
+
+ 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" + /> + +
)} - {/* Waiting for auth - user has clicked the link, waiting for token */} - {state.phase === "waiting_for_auth" && ( -
-
-
- - Waiting for authentication to complete... - -
-

- Complete the login in your browser. The token will be saved automatically. -

- - Open login page again - + {/* Submitting */} + {state.phase === "submitting" && ( +
+
+ + Submitting auth code... +
)} diff --git a/makima/frontend/tsconfig.tsbuildinfo b/makima/frontend/tsconfig.tsbuildinfo index 865c149..a063be7 100644 --- a/makima/frontend/tsconfig.tsbuildinfo +++ b/makima/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/phaseconfirmationnotification.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/commandmodepanel.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractcontextmenu.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phaseconfirmationmodal.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/directives/directivedag.tsx","./src/components/directives/directivedetail.tsx","./src/components/directives/directivelist.tsx","./src/components/directives/directivelogstream.tsx","./src/components/directives/orchestratorstepnode.tsx","./src/components/directives/stepnode.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/history/checkpointcard.tsx","./src/components/history/checkpointlist.tsx","./src/components/history/conversationmessage.tsx","./src/components/history/conversationview.tsx","./src/components/history/historyfilters.tsx","./src/components/history/resumecontrols.tsx","./src/components/history/timelineeventcard.tsx","./src/components/history/timelinelist.tsx","./src/components/history/index.ts","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/discusscontractmodal.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/branchtaskmodal.tsx","./src/components/mesh/contractcompletequestion.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/gitactionspanel.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/patcheslistpanel.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/mesh/worktreefilespanel.tsx","./src/components/orders/orderdetail.tsx","./src/components/orders/orderlist.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usecontracts.ts","./src/hooks/usedirectives.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usemultitasksubscription.ts","./src/hooks/useorders.ts","./src/hooks/usespeakwebsocket.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/contract-file.tsx","./src/routes/contracts.tsx","./src/routes/directives.tsx","./src/routes/files.tsx","./src/routes/history.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/orders.tsx","./src/routes/settings.tsx","./src/routes/speak.tsx","./src/types/messages.ts"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/phaseconfirmationnotification.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/commandmodepanel.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractcontextmenu.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phaseconfirmationmodal.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/directives/directivedag.tsx","./src/components/directives/directivedetail.tsx","./src/components/directives/directivelist.tsx","./src/components/directives/directivelogstream.tsx","./src/components/directives/orchestratorstepnode.tsx","./src/components/directives/stepnode.tsx","./src/components/directives/taskslideoutpanel.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/history/checkpointcard.tsx","./src/components/history/checkpointlist.tsx","./src/components/history/conversationmessage.tsx","./src/components/history/conversationview.tsx","./src/components/history/historyfilters.tsx","./src/components/history/resumecontrols.tsx","./src/components/history/timelineeventcard.tsx","./src/components/history/timelinelist.tsx","./src/components/history/index.ts","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/discusscontractmodal.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/branchtaskmodal.tsx","./src/components/mesh/contractcompletequestion.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/gitactionspanel.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/patcheslistpanel.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/mesh/worktreefilespanel.tsx","./src/components/orders/orderdetail.tsx","./src/components/orders/orderlist.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usecontracts.ts","./src/hooks/usedirectives.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usemultitasksubscription.ts","./src/hooks/useorders.ts","./src/hooks/usespeakwebsocket.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/contract-file.tsx","./src/routes/contracts.tsx","./src/routes/daemons.tsx","./src/routes/directives.tsx","./src/routes/files.tsx","./src/routes/history.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/orders.tsx","./src/routes/settings.tsx","./src/routes/speak.tsx","./src/types/messages.ts"],"version":"5.9.3"} \ No newline at end of file diff --git a/makima/src/daemon/task/manager.rs b/makima/src/daemon/task/manager.rs index df5e167..dd7df8a 100644 --- a/makima/src/daemon/task/manager.rs +++ b/makima/src/daemon/task/manager.rs @@ -117,7 +117,6 @@ fn get_auth_flow_storage() -> &'static std::sync::Mutex bool { let storage = get_auth_flow_storage(); if let Ok(mut guard) = storage.lock() { @@ -128,7 +127,7 @@ pub fn send_auth_code(code: &str) -> bool { } } } - tracing::warn!("No pending auth flow to send code to (this is expected with the new token-based flow)"); + tracing::warn!("No pending auth flow to send code to"); false } @@ -189,7 +188,7 @@ struct OAuthFlowResult { /// The token is automatically detected, saved to disk, and reported via the token_rx channel. async fn get_oauth_login_url(claude_command: &str) -> Option { use portable_pty::{native_pty_system, CommandBuilder, PtySize}; - use std::io::Read; + use std::io::{Read, Write}; tracing::info!("Spawning claude setup-token in PTY to get OAuth login URL"); @@ -235,8 +234,7 @@ async fn get_oauth_login_url(claude_command: &str) -> Option { } }; - // Take the writer - we keep it alive but don't need to write auth codes anymore - let _writer = match pair.master.take_writer() { + let mut writer = match pair.master.take_writer() { Ok(writer) => writer, Err(e) => { tracing::error!(error = %e, "Failed to take PTY writer"); @@ -245,12 +243,12 @@ async fn get_oauth_login_url(claude_command: &str) -> Option { }; // Create channels for communication + let (code_tx, code_rx) = std::sync::mpsc::channel::(); let (url_tx, url_rx) = std::sync::mpsc::channel::(); let (token_tx, token_rx) = tokio::sync::oneshot::channel::(); - // Also store a legacy code sender for backward compatibility (in case old server sends SubmitAuthCode) + // Store the code sender globally so it can be used when AUTH_CODE message arrives { - let (code_tx, _code_rx) = std::sync::mpsc::channel::(); let storage = get_auth_flow_storage(); if let Ok(mut guard) = storage.lock() { *guard = Some(code_tx); @@ -332,9 +330,36 @@ async fn get_oauth_login_url(claude_command: &str) -> Option { tracing::info!("setup-token reader thread ended (token_saved={})", token_saved); }); - // Spawn cleanup thread - waits for reader to finish and cleans up the child process + // Spawn writer thread - waits for auth code and writes it to PTY std::thread::spawn(move || { - tracing::debug!("setup-token cleanup thread: waiting for reader thread to finish..."); + tracing::info!("setup-token writer thread started, waiting for auth code (10 min timeout)"); + + // Wait for auth code from frontend (with long timeout - user needs time to authenticate) + match code_rx.recv_timeout(std::time::Duration::from_secs(600)) { + Ok(code) => { + tracing::info!(code_len = code.len(), "Received auth code from frontend, writing to PTY"); + // Write code followed by carriage return (Enter key in raw terminal mode) + let code_with_enter = format!("{}\r", code); + if let Err(e) = writer.write_all(code_with_enter.as_bytes()) { + tracing::error!(error = %e, "Failed to write auth code to PTY"); + } else if let Err(e) = writer.flush() { + tracing::error!(error = %e, "Failed to flush PTY writer"); + } else { + tracing::info!("Auth code written to setup-token PTY successfully"); + // Give Ink a moment to process, then send another Enter in case first was buffered + std::thread::sleep(std::time::Duration::from_millis(100)); + let _ = writer.write_all(b"\r"); + let _ = writer.flush(); + tracing::info!("Sent additional Enter keypress"); + } + } + Err(e) => { + tracing::info!(error = %e, "Auth code receive ended (timeout or channel closed)"); + } + } + + // Wait for reader thread to finish + tracing::debug!("Waiting for reader thread to finish..."); let _ = reader_handle.join(); // Wait for child to fully exit @@ -347,9 +372,6 @@ async fn get_oauth_login_url(claude_command: &str) -> Option { tracing::error!(error = %e, "Failed to wait for setup-token process"); } } - - // Keep _writer alive until here so PTY stays open - drop(_writer); }); // Wait for URL with timeout @@ -2015,18 +2037,36 @@ impl TaskManager { } }); } - DaemonCommand::SubmitAuthCode { request_id, code: _ } => { - // Deprecated: The new setup-token flow outputs tokens directly. - // This handler is kept for backward compatibility but is a no-op. - tracing::info!(request_id = %request_id, "Received auth code submission (deprecated - new flow auto-detects token)"); - let msg = DaemonMessage::ReauthStatus { - request_id, - status: "completed".to_string(), - login_url: None, - error: None, - token_saved: load_oauth_token().is_some(), - }; - let _ = self.ws_tx.send(msg).await; + DaemonCommand::SubmitAuthCode { request_id, code } => { + tracing::info!(request_id = %request_id, "Received auth code submission from server"); + let ws_tx = self.ws_tx.clone(); + + if send_auth_code(&code) { + tracing::info!(request_id = %request_id, "Auth code forwarded to setup-token for reauth"); + // Wait a short time then report completion + // (the setup-token process takes a moment to complete) + tokio::spawn(async move { + tokio::time::sleep(std::time::Duration::from_secs(3)).await; + let msg = DaemonMessage::ReauthStatus { + request_id, + status: "completed".to_string(), + login_url: None, + error: None, + token_saved: false, + }; + let _ = ws_tx.send(msg).await; + }); + } else { + tracing::warn!(request_id = %request_id, "No pending auth flow to receive code for reauth"); + let msg = DaemonMessage::ReauthStatus { + request_id, + status: "failed".to_string(), + login_url: None, + error: Some("No pending auth flow to receive the code. Try triggering reauth again.".to_string()), + token_saved: false, + }; + let _ = self.ws_tx.send(msg).await; + } } DaemonCommand::ApplyPatchToWorktree { target_task_id, -- cgit v1.2.3