summaryrefslogtreecommitdiff
path: root/makima/frontend
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-03-02 15:18:31 +0000
committerGitHub <noreply@github.com>2026-03-02 15:18:31 +0000
commit78cb861412850889424ae7d5ae5cd952a2b90295 (patch)
tree7a6eb0693457886dbe0eea84c0c1489724791f79 /makima/frontend
parent2bc1cd4717b587cd2b8ffccd723b62f888e61aa8 (diff)
downloadsoryu-78cb861412850889424ae7d5ae5cd952a2b90295.tar.gz
soryu-78cb861412850889424ae7d5ae5cd952a2b90295.zip
feat: move daemon reauth to daemons page, add contract-backed directive steps, rename Mesh to Exec (#84)
* feat: soryu-co/soryu - makima: Rename Mesh to Exec in navigation * WIP: heartbeat checkpoint * WIP: heartbeat checkpoint * WIP: heartbeat checkpoint * feat: soryu-co/soryu - makima: Add contract-backed steps to directive flow * WIP: heartbeat checkpoint
Diffstat (limited to 'makima/frontend')
-rw-r--r--makima/frontend/src/components/NavStrip.tsx2
-rw-r--r--makima/frontend/src/components/PhaseConfirmationNotification.tsx2
-rw-r--r--makima/frontend/src/components/SupervisorQuestionNotification.tsx2
-rw-r--r--makima/frontend/src/components/contracts/CommandModePanel.tsx2
-rw-r--r--makima/frontend/src/components/directives/DirectiveDAG.tsx2
-rw-r--r--makima/frontend/src/components/directives/OrchestratorStepNode.tsx2
-rw-r--r--makima/frontend/src/components/directives/StepNode.tsx25
-rw-r--r--makima/frontend/src/components/mesh/TaskOutput.tsx110
-rw-r--r--makima/frontend/src/lib/api.ts74
-rw-r--r--makima/frontend/src/main.tsx4
-rw-r--r--makima/frontend/src/routes/contracts.tsx6
-rw-r--r--makima/frontend/src/routes/daemons.tsx363
-rw-r--r--makima/frontend/src/routes/files.tsx2
-rw-r--r--makima/frontend/src/routes/login.tsx4
-rw-r--r--makima/frontend/src/routes/mesh.tsx22
15 files changed, 489 insertions, 133 deletions
diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx
index 9556458..17013ac 100644
--- a/makima/frontend/src/components/NavStrip.tsx
+++ b/makima/frontend/src/components/NavStrip.tsx
@@ -14,7 +14,7 @@ const NAV_LINKS: NavLink[] = [
{ label: "Directives", href: "/directives", requiresAuth: true },
{ label: "Orders", href: "/orders", requiresAuth: true },
{ label: "Contracts", href: "/contracts", requiresAuth: true },
- { label: "Mesh", href: "/mesh", requiresAuth: true },
+ { label: "Exec", href: "/exec", requiresAuth: true },
{ label: "Daemons", href: "/daemons", requiresAuth: true },
{ label: "History", href: "/history", requiresAuth: true },
];
diff --git a/makima/frontend/src/components/PhaseConfirmationNotification.tsx b/makima/frontend/src/components/PhaseConfirmationNotification.tsx
index 516211f..2681fdc 100644
--- a/makima/frontend/src/components/PhaseConfirmationNotification.tsx
+++ b/makima/frontend/src/components/PhaseConfirmationNotification.tsx
@@ -86,7 +86,7 @@ export function PhaseConfirmationToast() {
const handleGoToTask = (question: PendingQuestion) => {
dismissNotification(question.questionId);
- navigate(`/mesh/${question.taskId}`);
+ navigate(`/exec/${question.taskId}`);
};
return (
diff --git a/makima/frontend/src/components/SupervisorQuestionNotification.tsx b/makima/frontend/src/components/SupervisorQuestionNotification.tsx
index b1cbacc..e62638c 100644
--- a/makima/frontend/src/components/SupervisorQuestionNotification.tsx
+++ b/makima/frontend/src/components/SupervisorQuestionNotification.tsx
@@ -16,7 +16,7 @@ export function SupervisorQuestionNotification() {
const handleGoToTask = (questionId: string, taskId: string) => {
dismissNotification(questionId);
- navigate(`/mesh/${taskId}`);
+ navigate(`/exec/${taskId}`);
};
return (
diff --git a/makima/frontend/src/components/contracts/CommandModePanel.tsx b/makima/frontend/src/components/contracts/CommandModePanel.tsx
index 832d5ec..b39b309 100644
--- a/makima/frontend/src/components/contracts/CommandModePanel.tsx
+++ b/makima/frontend/src/components/contracts/CommandModePanel.tsx
@@ -65,7 +65,7 @@ export function CommandModePanel({ contract, onUpdate }: CommandModePanelProps)
const handleGoToSupervisor = useCallback(() => {
if (supervisorStatus.supervisorTaskId) {
- navigate(`/mesh/${supervisorStatus.supervisorTaskId}`);
+ navigate(`/exec/${supervisorStatus.supervisorTaskId}`);
}
}, [supervisorStatus.supervisorTaskId, navigate]);
const config = statusConfig[supervisorStatus.status];
diff --git a/makima/frontend/src/components/directives/DirectiveDAG.tsx b/makima/frontend/src/components/directives/DirectiveDAG.tsx
index 142df41..f225356 100644
--- a/makima/frontend/src/components/directives/DirectiveDAG.tsx
+++ b/makima/frontend/src/components/directives/DirectiveDAG.tsx
@@ -146,7 +146,7 @@ function SpecializedStepNode({ step }: { step: SpecializedStep }) {
{step.name}
</span>
<a
- href={`/mesh/${step.taskId}`}
+ href={`/exec/${step.taskId}`}
className="text-[9px] font-mono text-[#556677] hover:text-white underline"
>
View task
diff --git a/makima/frontend/src/components/directives/OrchestratorStepNode.tsx b/makima/frontend/src/components/directives/OrchestratorStepNode.tsx
index 9c8e95e..3fccb4b 100644
--- a/makima/frontend/src/components/directives/OrchestratorStepNode.tsx
+++ b/makima/frontend/src/components/directives/OrchestratorStepNode.tsx
@@ -151,7 +151,7 @@ export function OrchestratorStepNode({
{/* Task link */}
<a
- href={`/mesh/${taskId}`}
+ href={`/exec/${taskId}`}
className={`text-[9px] font-mono text-[#556677] hover:${colors.text} underline block`}
>
{status === "running" ? "View running task" : "View task"}
diff --git a/makima/frontend/src/components/directives/StepNode.tsx b/makima/frontend/src/components/directives/StepNode.tsx
index 2844b4a..775b898 100644
--- a/makima/frontend/src/components/directives/StepNode.tsx
+++ b/makima/frontend/src/components/directives/StepNode.tsx
@@ -28,10 +28,11 @@ interface StepNodeProps {
export function StepNode({ step, onComplete, onFail, onSkip }: StepNodeProps) {
const colors = STATUS_COLORS[step.status] || STATUS_COLORS.pending;
const label = STATUS_LABELS[step.status] || step.status.toUpperCase();
+ const isContractBacked = !!step.contractType;
return (
<div
- className={`${colors.bg} ${colors.border} border rounded px-3 py-2 min-w-[160px] max-w-[220px]`}
+ className={`${colors.bg} ${isContractBacked ? "border-2 border-dashed" : "border"} ${colors.border} rounded px-3 py-2 min-w-[160px] max-w-[220px]`}
>
<div className="flex items-center justify-between gap-2 mb-1">
<span className="text-[11px] font-mono text-white truncate font-medium">
@@ -41,14 +42,32 @@ export function StepNode({ step, onComplete, onFail, onSkip }: StepNodeProps) {
{label}
</span>
</div>
+ {isContractBacked && (
+ <div className="flex items-center gap-1 mb-1">
+ <span className="text-[8px] font-mono text-violet-400 bg-violet-400/10 border border-violet-400/20 rounded px-1 py-0.5 uppercase tracking-wide">
+ CONTRACT
+ </span>
+ <span className="text-[8px] font-mono text-violet-300/60">
+ {step.contractType}
+ </span>
+ </div>
+ )}
{step.description && (
<p className="text-[10px] text-[#7788aa] font-mono truncate mb-1">
{step.description}
</p>
)}
- {step.taskId && (
+ {step.contractId && (
+ <a
+ href={`/contracts/${step.contractId}`}
+ className="text-[9px] font-mono text-violet-400/60 hover:text-violet-300 underline block mb-1"
+ >
+ {step.status === "running" ? "Contract running..." : "View contract"}
+ </a>
+ )}
+ {step.taskId && !step.contractId && (
<a
- href={`/mesh/${step.taskId}`}
+ href={`/exec/${step.taskId}`}
className="text-[9px] font-mono text-[#556677] hover:text-[#75aafc] underline block mb-1"
>
{step.status === "running" ? "Auto-executing..." : "View task"}
diff --git a/makima/frontend/src/components/mesh/TaskOutput.tsx b/makima/frontend/src/components/mesh/TaskOutput.tsx
index 2db4250..1a376ad 100644
--- a/makima/frontend/src/components/mesh/TaskOutput.tsx
+++ b/makima/frontend/src/components/mesh/TaskOutput.tsx
@@ -309,7 +309,21 @@ function OutputEntryRenderer({ entry, pendingQuestionIds, onAnswerQuestion }: Ou
);
case "auth_required":
- return <AuthRequiredEntry entry={entry} />;
+ return (
+ <div className="bg-amber-900/30 border border-amber-500/50 rounded p-3 my-2">
+ <div className="flex items-center gap-2 text-amber-400 font-semibold mb-1">
+ <span>&#x26A0;</span>
+ <span>Authentication Expired{entry.toolInput?.hostname ? ` (${entry.toolInput.hostname})` : ""}</span>
+ </div>
+ <p className="text-amber-200/80 text-sm">
+ The daemon's OAuth token has expired. Go to the{" "}
+ <a href="/daemons" className="text-[#75aafc] hover:text-[#9bc3ff] underline">
+ Daemons page
+ </a>{" "}
+ to reauthorize, then retry this task.
+ </p>
+ </div>
+ );
case "supervisor_question":
return (
@@ -501,98 +515,8 @@ function SupervisorQuestionEntry({
);
}
-function AuthRequiredEntry({ entry }: { entry: TaskOutputEvent }) {
- const [authCode, setAuthCode] = useState("");
- const [submitting, setSubmitting] = useState(false);
- const [submitted, setSubmitted] = useState(false);
- const [error, setError] = useState<string | null>(null);
-
- const loginUrl = entry.toolInput?.loginUrl as string | undefined;
- const hostname = entry.toolInput?.hostname as string | undefined;
- // Get taskId from entry or fallback to toolInput (for robustness)
- const taskId = entry.taskId || (entry.toolInput?.taskId as string | undefined);
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- if (!authCode.trim() || !taskId) return;
-
- setSubmitting(true);
- setError(null);
-
- try {
- // Send the auth code to the task via the message endpoint
- await sendTaskMessage(taskId, `AUTH_CODE:${authCode.trim()}`);
- setSubmitted(true);
- } catch (err) {
- setError(err instanceof Error ? err.message : "Failed to submit code");
- } finally {
- setSubmitting(false);
- }
- };
-
- if (submitted) {
- return (
- <div className="bg-green-900/30 border border-green-500/50 rounded p-3 my-2">
- <div className="flex items-center gap-2 text-green-400 font-semibold">
- <span>✓</span>
- <span>Authentication code submitted</span>
- </div>
- <p className="text-green-200/80 text-sm mt-1">
- Waiting for authentication to complete...
- </p>
- </div>
- );
- }
-
- return (
- <div className="bg-amber-900/30 border border-amber-500/50 rounded p-3 my-2">
- <div className="flex items-center gap-2 text-amber-400 font-semibold mb-2">
- <span>🔐</span>
- <span>Authentication Required{hostname ? ` (${hostname})` : ""}</span>
- </div>
- <p className="text-amber-200/80 text-sm mb-3">
- The daemon's OAuth token has expired. Click the button to login, then paste the code below:
- </p>
-
- <div className="flex flex-col gap-3">
- {loginUrl ? (
- <a
- href={loginUrl}
- target="_blank"
- rel="noopener noreferrer"
- className="inline-block bg-amber-500 hover:bg-amber-400 text-black font-medium px-4 py-2 rounded transition-colors text-center"
- >
- 1. Login to Claude
- </a>
- ) : (
- <p className="text-red-400 text-sm">Login URL not available</p>
- )}
-
- <form onSubmit={handleSubmit} className="flex gap-2">
- <input
- type="text"
- value={authCode}
- onChange={(e) => setAuthCode(e.target.value)}
- placeholder="2. Paste authentication code here"
- className="flex-1 bg-[#0a1525] border border-amber-500/30 rounded px-3 py-2 text-amber-100 placeholder-amber-500/50 focus:outline-none focus:border-amber-400"
- disabled={submitting}
- />
- <button
- type="submit"
- disabled={submitting || !authCode.trim()}
- className="bg-amber-500 hover:bg-amber-400 disabled:bg-amber-700 disabled:cursor-not-allowed text-black font-medium px-4 py-2 rounded transition-colors"
- >
- {submitting ? "..." : "Submit"}
- </button>
- </form>
-
- {error && (
- <p className="text-red-400 text-sm">{error}</p>
- )}
- </div>
- </div>
- );
-}
+// AuthRequiredEntry has been removed - auth flow is now on the Daemons page.
+// The "auth_required" case in OutputEntryRenderer shows a redirect message instead.
/** Entry for phase transition confirmations */
function PhaseConfirmationEntry({
diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts
index 458b69d..155c716 100644
--- a/makima/frontend/src/lib/api.ts
+++ b/makima/frontend/src/lib/api.ts
@@ -1258,6 +1258,76 @@ export function getDaemonDownloadUrl(platform: string): string {
}
// =============================================================================
+// Daemon Reauthorization
+// =============================================================================
+
+/** Response from the trigger daemon reauth endpoint */
+export interface TriggerReauthResponse {
+ success: boolean;
+ daemonId: string;
+ requestId: string;
+}
+
+/** Response from the reauth status polling endpoint */
+export interface ReauthStatusResponse {
+ status: string; // "pending" | "url_ready" | "completed" | "failed"
+ loginUrl?: string;
+ error?: string;
+}
+
+/**
+ * Trigger OAuth re-authentication on a daemon.
+ * Sends a reauth command to the daemon, which will spawn `claude setup-token`
+ * and return the OAuth login URL.
+ */
+export async function triggerDaemonReauth(daemonId: string): Promise<TriggerReauthResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/daemons/${daemonId}/reauth`, {
+ method: "POST",
+ });
+ if (!res.ok) {
+ const errorData = await res.json().catch(() => ({}));
+ throw new Error(errorData.message || `Failed to trigger reauth: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/**
+ * Submit an OAuth auth code to a daemon's pending reauth flow.
+ */
+export async function submitDaemonAuthCode(
+ daemonId: string,
+ code: string,
+ requestId: string,
+): Promise<void> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/daemons/${daemonId}/reauth/code`, {
+ method: "POST",
+ body: JSON.stringify({ code, requestId }),
+ });
+ if (!res.ok) {
+ const errorData = await res.json().catch(() => ({}));
+ throw new Error(errorData.message || `Failed to submit auth code: ${res.statusText}`);
+ }
+}
+
+/**
+ * Get the status of a daemon reauth request.
+ * Used for polling to track reauth flow progress.
+ */
+export async function getDaemonReauthStatus(
+ daemonId: string,
+ requestId: string,
+): Promise<ReauthStatusResponse> {
+ const res = await authFetch(
+ `${API_BASE}/api/v1/mesh/daemons/${daemonId}/reauth/${requestId}/status`,
+ );
+ if (!res.ok) {
+ const errorData = await res.json().catch(() => ({}));
+ throw new Error(errorData.message || `Failed to get reauth status: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+// =============================================================================
// Mesh Chat Types for Task Orchestration
// =============================================================================
@@ -3081,6 +3151,10 @@ export interface DirectiveStep {
dependsOn: string[];
status: StepStatus;
taskId: string | null;
+ /** Contract ID for contract-backed steps */
+ contractId?: string | null;
+ /** Contract type (e.g. "simple", "specification", "execute") for contract-backed steps */
+ contractType?: string | null;
orderIndex: number;
generation: number;
startedAt: string | null;
diff --git a/makima/frontend/src/main.tsx b/makima/frontend/src/main.tsx
index 32c05ba..4f7c525 100644
--- a/makima/frontend/src/main.tsx
+++ b/makima/frontend/src/main.tsx
@@ -98,7 +98,7 @@ createRoot(document.getElementById("root")!).render(
}
/>
<Route
- path="/mesh"
+ path="/exec"
element={
<ProtectedRoute>
<MeshPage />
@@ -106,7 +106,7 @@ createRoot(document.getElementById("root")!).render(
}
/>
<Route
- path="/mesh/:id"
+ path="/exec/:id"
element={
<ProtectedRoute>
<MeshPage />
diff --git a/makima/frontend/src/routes/contracts.tsx b/makima/frontend/src/routes/contracts.tsx
index b85d667..ce9ceca 100644
--- a/makima/frontend/src/routes/contracts.tsx
+++ b/makima/frontend/src/routes/contracts.tsx
@@ -448,7 +448,7 @@ function ContractsPageContent() {
const handleTaskSelect = useCallback(
(taskId: string) => {
- navigate(`/mesh/${taskId}`);
+ navigate(`/exec/${taskId}`);
},
[navigate]
);
@@ -469,7 +469,7 @@ function ContractsPageContent() {
const refreshed = await fetchContract(contractDetail.id);
setContractDetail(refreshed);
// Navigate to the new task
- navigate(`/mesh/${task.id}`);
+ navigate(`/exec/${task.id}`);
} catch (e) {
console.error("Failed to create task:", e);
alert(e instanceof Error ? e.message : "Failed to create task");
@@ -515,7 +515,7 @@ function ContractsPageContent() {
const handleContextGoToSupervisor = useCallback(
(contract: ContractSummary) => {
if (contract.supervisorTaskId) {
- navigate(`/mesh/${contract.supervisorTaskId}`);
+ navigate(`/exec/${contract.supervisorTaskId}`);
}
},
[navigate]
diff --git a/makima/frontend/src/routes/daemons.tsx b/makima/frontend/src/routes/daemons.tsx
index 0f55190..ca167fe 100644
--- a/makima/frontend/src/routes/daemons.tsx
+++ b/makima/frontend/src/routes/daemons.tsx
@@ -1,10 +1,13 @@
-import { useState, useEffect } from "react";
+import { useState, useEffect, useCallback, useRef } from "react";
import { useAuth } from "../contexts/AuthContext";
import { useNavigate } from "react-router";
import { Masthead } from "../components/Masthead";
import {
listDaemons,
restartDaemon,
+ triggerDaemonReauth,
+ submitDaemonAuthCode,
+ getDaemonReauthStatus,
type Daemon,
type DaemonListResponse,
} from "../lib/api";
@@ -34,6 +37,333 @@ function ErrorAlert({ children }: { children: React.ReactNode }) {
}
// =============================================================================
+// Reauth Modal Component
+// =============================================================================
+
+type ReauthState =
+ | { phase: "initiating" }
+ | { phase: "url_ready"; loginUrl: string; requestId: string }
+ | { phase: "submitting"; requestId: string }
+ | { phase: "success" }
+ | { phase: "error"; message: string };
+
+function ReauthModal({
+ daemon,
+ onClose,
+}: {
+ daemon: Daemon;
+ 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
+ useEffect(() => {
+ return () => {
+ if (pollingRef.current) {
+ clearInterval(pollingRef.current);
+ }
+ };
+ }, []);
+
+ // Trigger reauth on mount
+ useEffect(() => {
+ let cancelled = false;
+ const trigger = async () => {
+ 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);
+ } catch (err) {
+ if (cancelled) return;
+ setState({
+ phase: "error",
+ message:
+ err instanceof Error ? err.message : "Failed to trigger reauth",
+ });
+ }
+ };
+ trigger();
+ 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",
+ });
+ }
+ },
+ [authCode, daemon.id, state],
+ );
+
+ 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);
+ } catch (err) {
+ setState({
+ phase: "error",
+ message:
+ err instanceof Error
+ ? err.message
+ : "Failed to trigger reauth",
+ });
+ }
+ };
+ trigger();
+ }, [daemon.id]);
+
+ return (
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
+ <div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.35)] p-6 max-w-md w-full mx-4 shadow-2xl">
+ {/* Header */}
+ <div className="flex items-center justify-between mb-4">
+ <h3 className="text-xs font-mono uppercase tracking-wide text-[#9bc3ff]">
+ Reauthorize Daemon
+ </h3>
+ <button
+ onClick={onClose}
+ className="text-[#7788aa] hover:text-[#9bc3ff] text-sm font-mono"
+ >
+ &#x2715;
+ </button>
+ </div>
+ <p className="text-[10px] font-mono text-[#7788aa] mb-4">
+ {daemon.hostname || "Unknown Host"}
+ </p>
+
+ {/* Initiating */}
+ {state.phase === "initiating" && (
+ <div className="flex items-center gap-2 py-4">
+ <div className="w-3 h-3 border border-[#75aafc] border-t-transparent rounded-full animate-spin" />
+ <span className="text-[10px] font-mono text-[#7788aa]">
+ Initiating reauthorization...
+ </span>
+ </div>
+ )}
+
+ {/* 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, then paste the code:
+ </p>
+ <a
+ href={state.loginUrl}
+ target="_blank"
+ rel="noopener noreferrer"
+ 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
+ </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>
+ )}
+
+ {/* 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>
+ )}
+
+ {/* Success */}
+ {state.phase === "success" && (
+ <div className="py-4">
+ <div className="flex items-center gap-2 text-green-400 mb-2">
+ <span className="text-sm">&#x2713;</span>
+ <span className="text-xs font-mono font-medium">
+ Authentication successful
+ </span>
+ </div>
+ <p className="text-[10px] font-mono text-[#7788aa] mb-3">
+ The daemon's OAuth token has been refreshed. Tasks can now run normally.
+ </p>
+ <button
+ onClick={onClose}
+ className="text-[10px] font-mono text-[#75aafc] hover:text-[#9bc3ff]"
+ >
+ Close
+ </button>
+ </div>
+ )}
+
+ {/* Error */}
+ {state.phase === "error" && (
+ <div className="py-4">
+ <div className="border border-red-700/50 bg-red-900/20 text-red-400 px-3 py-2 mb-3 font-mono text-xs">
+ {state.message}
+ </div>
+ <div className="flex gap-2">
+ <button
+ onClick={handleRetry}
+ className="text-[10px] font-mono text-amber-400 hover:text-amber-300 px-2 py-1 border border-amber-700/50 bg-amber-900/20"
+ >
+ Retry
+ </button>
+ <button
+ onClick={onClose}
+ className="text-[10px] font-mono text-[#7788aa] hover:text-[#9bc3ff] px-2 py-1"
+ >
+ Close
+ </button>
+ </div>
+ </div>
+ )}
+ </div>
+ </div>
+ );
+}
+
+// =============================================================================
// Daemons Page
// =============================================================================
@@ -47,6 +377,7 @@ export default function DaemonsPage() {
const [daemonsError, setDaemonsError] = useState<string | null>(null);
const [restartingDaemonId, setRestartingDaemonId] = useState<string | null>(null);
const [restartConfirmDaemonId, setRestartConfirmDaemonId] = useState<string | null>(null);
+ const [reauthDaemon, setReauthDaemon] = useState<Daemon | null>(null);
// Redirect if not authenticated
useEffect(() => {
@@ -292,7 +623,7 @@ export default function DaemonsPage() {
</div>
)}
</div>
- {/* Restart Section */}
+ {/* Actions Section */}
{daemon.status === "connected" && (
<div className="mt-3 pt-2 border-t border-[rgba(117,170,252,0.1)]">
{restartConfirmDaemonId === daemon.id ? (
@@ -318,12 +649,20 @@ export default function DaemonsPage() {
</div>
</div>
) : (
- <button
- onClick={() => setRestartConfirmDaemonId(daemon.id)}
- className="text-[10px] font-mono text-[#75aafc] hover:text-[#9bc3ff]"
- >
- &#x27F3; Restart Daemon
- </button>
+ <div className="flex items-center gap-3">
+ <button
+ onClick={() => setReauthDaemon(daemon)}
+ className="text-[10px] font-mono text-amber-400 hover:text-amber-300"
+ >
+ &#x1F511; Reauthorize
+ </button>
+ <button
+ onClick={() => setRestartConfirmDaemonId(daemon.id)}
+ className="text-[10px] font-mono text-[#75aafc] hover:text-[#9bc3ff]"
+ >
+ &#x27F3; Restart Daemon
+ </button>
+ </div>
)}
</div>
)}
@@ -335,6 +674,14 @@ export default function DaemonsPage() {
</div>
</div>
</main>
+
+ {/* Reauth Modal */}
+ {reauthDaemon && (
+ <ReauthModal
+ daemon={reauthDaemon}
+ onClose={() => setReauthDaemon(null)}
+ />
+ )}
</div>
);
}
diff --git a/makima/frontend/src/routes/files.tsx b/makima/frontend/src/routes/files.tsx
index 6cfb3ca..b232aa0 100644
--- a/makima/frontend/src/routes/files.tsx
+++ b/makima/frontend/src/routes/files.tsx
@@ -839,7 +839,7 @@ function FilesPageContent() {
<div className="flex gap-2">
<button
onClick={() => {
- navigate(`/mesh/${createdTask.id}`);
+ navigate(`/exec/${createdTask.id}`);
setCreatedTask(null);
}}
className="px-3 py-1 font-mono text-xs text-[#0a1628] bg-[#75aafc] hover:bg-[#9bc3ff] transition-colors"
diff --git a/makima/frontend/src/routes/login.tsx b/makima/frontend/src/routes/login.tsx
index 63b3af3..0725a2d 100644
--- a/makima/frontend/src/routes/login.tsx
+++ b/makima/frontend/src/routes/login.tsx
@@ -18,7 +18,7 @@ export default function LoginPage() {
// Redirect if already authenticated
if (isAuthenticated && isAuthConfigured) {
- navigate("/mesh");
+ navigate("/exec");
return null;
}
@@ -34,7 +34,7 @@ export default function LoginPage() {
if (error) {
setError(error.message);
} else {
- navigate("/mesh");
+ navigate("/exec");
}
} else if (mode === "signup") {
const { error } = await signUp(email, password);
diff --git a/makima/frontend/src/routes/mesh.tsx b/makima/frontend/src/routes/mesh.tsx
index 1d1db84..67129f9 100644
--- a/makima/frontend/src/routes/mesh.tsx
+++ b/makima/frontend/src/routes/mesh.tsx
@@ -191,14 +191,6 @@ export default function MeshPage() {
// Only process output for the task we're currently viewing
if (event.taskId === activeOutputTaskId) {
setTaskOutputEntries((prev) => {
- // For auth_required, only allow one per task (replace existing)
- if (event.messageType === "auth_required") {
- const hasExisting = prev.some(e => e.messageType === "auth_required");
- if (hasExisting) {
- return prev; // Skip duplicate auth_required
- }
- }
-
// Deduplicate by checking if last entry is identical
// This prevents duplicates from React StrictMode or WebSocket reconnects
const lastEntry = prev[prev.length - 1];
@@ -387,7 +379,7 @@ export default function MeshPage() {
const handleSelectTask = useCallback(
(taskId: string) => {
- navigate(`/mesh/${taskId}`);
+ navigate(`/exec/${taskId}`);
},
[navigate]
);
@@ -395,9 +387,9 @@ export default function MeshPage() {
const handleBack = useCallback(() => {
// If viewing a subtask, go back to parent
if (taskDetail?.parentTaskId) {
- navigate(`/mesh/${taskDetail.parentTaskId}`);
+ navigate(`/exec/${taskDetail.parentTaskId}`);
} else {
- navigate("/mesh");
+ navigate("/exec");
}
}, [navigate, taskDetail]);
@@ -408,9 +400,9 @@ export default function MeshPage() {
if (success && id === taskId) {
// If deleting current task, go back
if (taskDetail?.parentTaskId) {
- navigate(`/mesh/${taskDetail.parentTaskId}`);
+ navigate(`/exec/${taskDetail.parentTaskId}`);
} else {
- navigate("/mesh");
+ navigate("/exec");
}
}
}
@@ -523,7 +515,7 @@ export default function MeshPage() {
});
console.log(`[Mesh] Task branched, new task ID: ${result.task.id}`);
// Navigate to the new branched task
- navigate(`/mesh/${result.task.id}`);
+ navigate(`/exec/${result.task.id}`);
} catch (e) {
console.error("Failed to branch task:", e);
throw e; // Re-throw so the modal can display the error
@@ -617,7 +609,7 @@ export default function MeshPage() {
targetRepoPath: targetPath || undefined,
});
if (newTask) {
- navigate(`/mesh/${newTask.id}`);
+ navigate(`/exec/${newTask.id}`);
}
} finally {
setCreating(false);