summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--makima/frontend/src/routes/daemons.tsx134
-rw-r--r--makima/frontend/tsconfig.tsbuildinfo2
-rw-r--r--makima/src/daemon/task/manager.rs88
3 files changed, 156 insertions, 68 deletions
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<ReauthState>({ phase: "initiating" });
+ const [authCode, setAuthCode] = useState("");
const pollingRef = useRef<ReturnType<typeof setInterval> | 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({
</div>
)}
- {/* URL Ready - user needs to click the link */}
+ {/* URL Ready */}
{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. Authentication will complete automatically.
+ Click the button below to open the OAuth login page, then paste the code:
</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"
>
- Login to Claude
+ 1. Login to Claude
</a>
- <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>
+ <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>
)}
- {/* 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>
+ {/* 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>
</div>
)}
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<Option<std::sync::mpsc::
}
/// Send an auth code to the pending OAuth flow.
-/// Deprecated: The new setup-token flow outputs tokens directly, so this is no longer needed.
pub fn send_auth_code(code: &str) -> 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<OAuthFlowResult> {
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<OAuthFlowResult> {
}
};
- // 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<OAuthFlowResult> {
};
// Create channels for communication
+ let (code_tx, code_rx) = std::sync::mpsc::channel::<String>();
let (url_tx, url_rx) = std::sync::mpsc::channel::<String>();
let (token_tx, token_rx) = tokio::sync::oneshot::channel::<String>();
- // 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::<String>();
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<OAuthFlowResult> {
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<OAuthFlowResult> {
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,