An autopilot per thread continues the user's order run after run, with no time limit. Each run reports a checkpoint; after three runs without progress or three failed runs it pauses with Continue and Stop. Start, pause, stop and finish show as quiet lines in the conversation. Approvals during an autopilot run are parked as suggestions, and the temporary approval rules end with the autopilot. Claude-Session: https://claude.ai/code/session_01UmSCnoCgXqBp2fU3vwz4o1 Co-authored-by: Claude <noreply@anthropic.com>
126 lines
4.0 KiB
TypeScript
126 lines
4.0 KiB
TypeScript
import type { AutopilotBlock } from "@rakazo/contracts";
|
|
import { formatAutopilotDuration } from "@rakazo/core";
|
|
import { useState } from "react";
|
|
import { Pressable, Text, View } from "react-native";
|
|
import { dateLocaleForUi, useI18n } from "../lib/i18n";
|
|
import { useMobileTokens } from "../lib/native";
|
|
import { NativeSymbol } from "./native-symbol";
|
|
|
|
export type AutopilotAction = "stop" | "resume";
|
|
|
|
const ICONS = {
|
|
"autopilot.started": { ios: "play.fill", android: "play" },
|
|
"autopilot.paused": { ios: "pause.fill", android: "pause" },
|
|
"autopilot.stopped": { ios: "stop.fill", android: "stop" },
|
|
"autopilot.finished": { ios: "checkmark.circle", android: "checkmark-circle-outline" },
|
|
} as const;
|
|
|
|
/** One quiet line when the autopilot starts, pauses, stops or finishes. */
|
|
export function AutopilotLine({
|
|
block,
|
|
onAction,
|
|
}: {
|
|
block: AutopilotBlock;
|
|
onAction?: (action: AutopilotAction) => Promise<void>;
|
|
}) {
|
|
const tokens = useMobileTokens();
|
|
const { t, locale } = useI18n();
|
|
const [busy, setBusy] = useState(false);
|
|
const [failed, setFailed] = useState(false);
|
|
const open = !block.ended && Boolean(onAction);
|
|
const muted = { color: tokens.mutedForeground, fontSize: 13.5 };
|
|
|
|
async function act(action: AutopilotAction) {
|
|
if (!onAction) return;
|
|
setBusy(true);
|
|
setFailed(false);
|
|
try {
|
|
await onAction(action);
|
|
} catch {
|
|
setFailed(true);
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
const parts: string[] = [];
|
|
let label = t("Autopilot");
|
|
if (block.event === "autopilot.started") {
|
|
parts.push(block.goal);
|
|
if (block.until) {
|
|
const until = new Intl.DateTimeFormat(dateLocaleForUi(locale), {
|
|
hour: "numeric",
|
|
minute: "2-digit",
|
|
}).format(new Date(block.until));
|
|
parts.push(t("until {until}", { until }));
|
|
}
|
|
if (block.focus) parts.push(block.focus);
|
|
if (block.allowed?.length) {
|
|
parts.push(t("allowed: {allowed}", { allowed: block.allowed.join(", ") }));
|
|
}
|
|
} else if (block.event === "autopilot.paused") {
|
|
label = t("Autopilot paused");
|
|
if (block.text) parts.push(block.text);
|
|
} else {
|
|
label = block.event === "autopilot.stopped" ? t("Autopilot stopped") : t("Autopilot finished");
|
|
if (block.durationMs !== undefined) parts.push(formatAutopilotDuration(block.durationMs));
|
|
}
|
|
const icon = ICONS[block.event];
|
|
const link = (text: string, action: AutopilotAction) => (
|
|
<Pressable
|
|
accessibilityRole="button"
|
|
hitSlop={10}
|
|
disabled={busy}
|
|
onPress={() => void act(action)}
|
|
style={{ opacity: busy ? 0.5 : 1 }}
|
|
>
|
|
<Text style={[muted, { fontWeight: "600" }]}>{text}</Text>
|
|
</Pressable>
|
|
);
|
|
|
|
return (
|
|
<View style={{ width: "100%", paddingVertical: 4, alignItems: "center", gap: 4 }}>
|
|
<View
|
|
style={{
|
|
flexDirection: "row",
|
|
flexWrap: "wrap",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
columnGap: 6,
|
|
rowGap: 2,
|
|
}}
|
|
>
|
|
<NativeSymbol
|
|
ios={icon.ios}
|
|
android={icon.android}
|
|
size={13}
|
|
color={tokens.mutedForeground}
|
|
/>
|
|
<Text style={[muted, { color: tokens.foreground, fontWeight: "500" }]}>{label}</Text>
|
|
{parts.map((part, index) => (
|
|
<Text key={index} style={muted}>
|
|
· {part}
|
|
</Text>
|
|
))}
|
|
{open && block.event === "autopilot.paused" ? (
|
|
<>
|
|
<Text style={muted}>·</Text>
|
|
{link(t("Continue"), "resume")}
|
|
</>
|
|
) : null}
|
|
{open && (block.event === "autopilot.started" || block.event === "autopilot.paused") ? (
|
|
<>
|
|
<Text style={muted}>·</Text>
|
|
{link(failed ? t("Try again") : t("Stop"), "stop")}
|
|
</>
|
|
) : null}
|
|
</View>
|
|
{block.event === "autopilot.finished" && block.text ? (
|
|
<Text style={{ color: tokens.foreground, fontSize: 14, textAlign: "center" }}>
|
|
{block.text}
|
|
</Text>
|
|
) : null}
|
|
</View>
|
|
);
|
|
}
|