feat: autopilot progress, composer start and morning summary (#146)
The autopilot's start line counts its steps and folds open to the latest checkpoints, and its result reads as a card. The composer's plus menu starts an autopilot with the draft as the order and a choice of until done, tomorrow morning or a time. Today lists the autopilots that run, wait or ended within a day, and their step runs no longer show one by one. Autopilot runs send no completion or failure push; only a pause notifies. Claude-Session: https://claude.ai/code/session_01UmSCnoCgXqBp2fU3vwz4o1 Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c69f00e891
commit
54658ee689
@ -60,6 +60,8 @@ One bot has one continuous visible thread because continuity is part of its iden
|
|||||||
Team Computers share a persisted browser identity and isolate simultaneous live desktops when supported because shared login state and safe concurrency are both required.
|
Team Computers share a persisted browser identity and isolate simultaneous live desktops when supported because shared login state and safe concurrency are both required.
|
||||||
Private Computers isolate the entire working home because some trust boundaries must be stronger than collaboration convenience.
|
Private Computers isolate the entire working home because some trust boundaries must be stronger than collaboration convenience.
|
||||||
Connections belong to the user or space rather than requiring repetitive per-bot setup because bots differ through role and state, not account plumbing.
|
Connections belong to the user or space rather than requiring repetitive per-bot setup because bots differ through role and state, not account plumbing.
|
||||||
|
Bots answer with cards from a fixed catalog of sections rather than generated interface because the backend must validate every card and apply every tap, and a small catalog stays calm and consistent on every surface.
|
||||||
|
An autopilot works on an order with no time or cost limit, but pauses when runs stop reporting progress and always shows when it started and ended, because long unattended work must stay visible and must never spin silently.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,8 @@ import {
|
|||||||
resumeAutopilot,
|
resumeAutopilot,
|
||||||
startAutopilot,
|
startAutopilot,
|
||||||
} from "@rakazo/adapters";
|
} from "@rakazo/adapters";
|
||||||
import type { Actor, Autopilot } from "@rakazo/contracts";
|
import type { Actor, Autopilot, AutopilotRecent } from "@rakazo/contracts";
|
||||||
|
import { AUTOPILOT_RECENT_MS } from "@rakazo/contracts";
|
||||||
import type { PrismaClient, ThreadEvents } from "@rakazo/db";
|
import type { PrismaClient, ThreadEvents } from "@rakazo/db";
|
||||||
import { IsolationError } from "@rakazo/db";
|
import { IsolationError } from "@rakazo/db";
|
||||||
import type { ThreadTarget } from "./thread-target.js";
|
import type { ThreadTarget } from "./thread-target.js";
|
||||||
@ -77,3 +78,29 @@ export async function resumeThreadAutopilot(
|
|||||||
}
|
}
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Today's view: the user's open autopilots in this space and those that ended within a day. */
|
||||||
|
export async function listRecentAutopilots(
|
||||||
|
deps: Pick<AutopilotDeps, "prisma">,
|
||||||
|
actor: Actor,
|
||||||
|
now = new Date(),
|
||||||
|
): Promise<AutopilotRecent[]> {
|
||||||
|
const rows = await deps.prisma.autopilot.findMany({
|
||||||
|
where: {
|
||||||
|
spaceId: actor.spaceId,
|
||||||
|
userId: actor.userId,
|
||||||
|
OR: [
|
||||||
|
{ status: { in: ["active", "paused"] } },
|
||||||
|
{ endedAt: { gte: new Date(now.getTime() - AUTOPILOT_RECENT_MS) } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
include: { thread: { select: { groupId: true } } },
|
||||||
|
orderBy: { startedAt: "desc" },
|
||||||
|
take: 10,
|
||||||
|
});
|
||||||
|
return rows.map((row) => ({
|
||||||
|
...autopilotView(row),
|
||||||
|
spaceId: row.spaceId,
|
||||||
|
groupId: row.thread.groupId,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|||||||
@ -173,6 +173,7 @@ import { aiConsentStatus, allowAiConsent } from "./ai-consent.js";
|
|||||||
import { createOwnedArtifact, getOwnedArtifact, getSpaceArtifact } from "./artifacts.js";
|
import { createOwnedArtifact, getOwnedArtifact, getSpaceArtifact } from "./artifacts.js";
|
||||||
import {
|
import {
|
||||||
getThreadAutopilot,
|
getThreadAutopilot,
|
||||||
|
listRecentAutopilots,
|
||||||
resumeThreadAutopilot,
|
resumeThreadAutopilot,
|
||||||
startThreadAutopilot,
|
startThreadAutopilot,
|
||||||
stopThreadAutopilot,
|
stopThreadAutopilot,
|
||||||
@ -5173,6 +5174,9 @@ export function createRouter(deps: RouterDeps) {
|
|||||||
const target = await resolveThreadTarget(deps.prisma, context.actor, input);
|
const target = await resolveThreadTarget(deps.prisma, context.actor, input);
|
||||||
return resumeThreadAutopilot(deps, context.actor, target);
|
return resumeThreadAutopilot(deps, context.actor, target);
|
||||||
}),
|
}),
|
||||||
|
recent: authed.autopilot.recent.handler(async ({ context }) => ({
|
||||||
|
items: await listRecentAutopilots(deps, context.actor),
|
||||||
|
})),
|
||||||
},
|
},
|
||||||
voice: {
|
voice: {
|
||||||
catalog: authed.voice.catalog.handler(async () => listVoiceCatalog()),
|
catalog: authed.voice.catalog.handler(async () => listVoiceCatalog()),
|
||||||
|
|||||||
@ -20,6 +20,16 @@ describe("run activity copy", () => {
|
|||||||
).toBe("Maya asked: Please check the release workflow.");
|
).toBe("Maya asked: Please check the release workflow.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("names an autopilot step by the user's order", () => {
|
||||||
|
expect(
|
||||||
|
activityPromptSnippet({
|
||||||
|
trigger: "autopilot",
|
||||||
|
prompt:
|
||||||
|
"Autopilot: continue the user's order on your own.\nOrder: Tidy the tickets\nUntil: done",
|
||||||
|
}),
|
||||||
|
).toBe("Tidy the tickets");
|
||||||
|
});
|
||||||
|
|
||||||
it("names a delegated task by its title instead of its wake prompt", () => {
|
it("names a delegated task by its title instead of its wake prompt", () => {
|
||||||
expect(
|
expect(
|
||||||
activityPromptSnippet({
|
activityPromptSnippet({
|
||||||
|
|||||||
@ -26,6 +26,10 @@ export function activityPromptSnippet(
|
|||||||
const task = parsed.success ? taskBlockOf(parsed.data) : undefined;
|
const task = parsed.success ? taskBlockOf(parsed.data) : undefined;
|
||||||
return promptSnippet(task?.title ?? input.prompt, max);
|
return promptSnippet(task?.title ?? input.prompt, max);
|
||||||
}
|
}
|
||||||
|
if (input.trigger === "autopilot") {
|
||||||
|
// The continuation prompt is for the model; people know the run by its order.
|
||||||
|
return promptSnippet(/^Order: (.*)$/m.exec(input.prompt)?.[1] ?? input.prompt, max);
|
||||||
|
}
|
||||||
if (input.trigger !== "bot_message") return promptSnippet(input.prompt, max);
|
if (input.trigger !== "bot_message") return promptSnippet(input.prompt, max);
|
||||||
const parsed = MessageBlock.array().safeParse(input.sourceBlocks);
|
const parsed = MessageBlock.array().safeParse(input.sourceBlocks);
|
||||||
const message = parsed.success ? botMessageContext(parsed.data) : undefined;
|
const message = parsed.success ? botMessageContext(parsed.data) : undefined;
|
||||||
|
|||||||
@ -9,13 +9,16 @@ import type {
|
|||||||
Routine,
|
Routine,
|
||||||
} from "@rakazo/contracts";
|
} from "@rakazo/contracts";
|
||||||
import {
|
import {
|
||||||
|
AUTOPILOT_LIMITS,
|
||||||
canReactToThreadMessage,
|
canReactToThreadMessage,
|
||||||
MESSAGE_REACTIONS,
|
MESSAGE_REACTIONS,
|
||||||
type MessageReaction,
|
type MessageReaction,
|
||||||
} from "@rakazo/contracts";
|
} from "@rakazo/contracts";
|
||||||
|
import type { AutopilotUntilChoice } from "@rakazo/core";
|
||||||
import {
|
import {
|
||||||
abortableDelay,
|
abortableDelay,
|
||||||
attachmentsForThread,
|
attachmentsForThread,
|
||||||
|
autopilotUntil,
|
||||||
buildComposerMentionOptions,
|
buildComposerMentionOptions,
|
||||||
type ComposerMention,
|
type ComposerMention,
|
||||||
cloudAgentHttpsUrl,
|
cloudAgentHttpsUrl,
|
||||||
@ -1277,6 +1280,8 @@ function Thread() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
function showAttachMenu() {
|
function showAttachMenu() {
|
||||||
|
// The autopilot takes the typed order, so it is offered once there is one.
|
||||||
|
const order = draft.trim();
|
||||||
Alert.alert(t("Attach"), undefined, [
|
Alert.alert(t("Attach"), undefined, [
|
||||||
{
|
{
|
||||||
text: t("Photo library"),
|
text: t("Photo library"),
|
||||||
@ -1284,6 +1289,34 @@ function Thread() {
|
|||||||
},
|
},
|
||||||
{ text: t("Camera"), onPress: () => void addAttachments(takePhoto) },
|
{ text: t("Camera"), onPress: () => void addAttachments(takePhoto) },
|
||||||
{ text: t("File"), onPress: () => void addAttachments(pickDocuments) },
|
{ text: t("File"), onPress: () => void addAttachments(pickDocuments) },
|
||||||
|
...(order ? [{ text: t("Autopilot"), onPress: () => askAutopilotUntil(order) }] : []),
|
||||||
|
{ text: t("Cancel"), style: "cancel" as const },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function askAutopilotUntil(order: string) {
|
||||||
|
const start = (choice: AutopilotUntilChoice) => {
|
||||||
|
const targetBotId = botId;
|
||||||
|
const targetGroupId = groupId;
|
||||||
|
if (!targetBotId && !targetGroupId) return;
|
||||||
|
const target = targetGroupId ? { groupId: targetGroupId } : { botId: targetBotId! };
|
||||||
|
const until = autopilotUntil(choice, new Date());
|
||||||
|
void rpc("autopilot/start", {
|
||||||
|
...target,
|
||||||
|
goal: order.slice(0, AUTOPILOT_LIMITS.goal),
|
||||||
|
...(until ? { until: until.toISOString() } : {}),
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
setDraft("");
|
||||||
|
if (isCurrentTarget(targetBotId, targetGroupId)) await refresh();
|
||||||
|
})
|
||||||
|
.catch((err) =>
|
||||||
|
Alert.alert(t("Autopilot"), err instanceof Error ? err.message : t("Try again.")),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
Alert.alert(t("Until when?"), order, [
|
||||||
|
{ text: t("Until done"), onPress: () => start({ kind: "done" }) },
|
||||||
|
{ text: t("Until tomorrow morning"), onPress: () => start({ kind: "morning" }) },
|
||||||
{ text: t("Cancel"), style: "cancel" },
|
{ text: t("Cancel"), style: "cancel" },
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,10 @@ const ICONS = {
|
|||||||
"autopilot.finished": { ios: "checkmark.circle", android: "checkmark-circle-outline" },
|
"autopilot.finished": { ios: "checkmark.circle", android: "checkmark-circle-outline" },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/** One quiet line when the autopilot starts, pauses, stops or finishes. */
|
/**
|
||||||
|
* One quiet line when the autopilot starts, pauses, stops or finishes. The start line counts the
|
||||||
|
* steps and folds open to the latest checkpoints; a finished autopilot's result reads as a card.
|
||||||
|
*/
|
||||||
export function AutopilotLine({
|
export function AutopilotLine({
|
||||||
block,
|
block,
|
||||||
onAction,
|
onAction,
|
||||||
@ -27,6 +30,7 @@ export function AutopilotLine({
|
|||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [failed, setFailed] = useState(false);
|
const [failed, setFailed] = useState(false);
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
const open = !block.ended && Boolean(onAction);
|
const open = !block.ended && Boolean(onAction);
|
||||||
const muted = { color: tokens.mutedForeground, fontSize: 13.5 };
|
const muted = { color: tokens.mutedForeground, fontSize: 13.5 };
|
||||||
|
|
||||||
@ -66,6 +70,12 @@ export function AutopilotLine({
|
|||||||
if (block.durationMs !== undefined) parts.push(formatAutopilotDuration(block.durationMs));
|
if (block.durationMs !== undefined) parts.push(formatAutopilotDuration(block.durationMs));
|
||||||
}
|
}
|
||||||
const icon = ICONS[block.event];
|
const icon = ICONS[block.event];
|
||||||
|
const checkpoints = block.event === "autopilot.started" ? (block.checkpoints ?? []) : [];
|
||||||
|
const latest = checkpoints.at(-1)?.text;
|
||||||
|
const time = new Intl.DateTimeFormat(dateLocaleForUi(locale), {
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
const link = (text: string, action: AutopilotAction) => (
|
const link = (text: string, action: AutopilotAction) => (
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
@ -102,6 +112,26 @@ export function AutopilotLine({
|
|||||||
· {part}
|
· {part}
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
|
{block.steps && block.event === "autopilot.started" ? (
|
||||||
|
<>
|
||||||
|
<Text style={muted}>·</Text>
|
||||||
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityState={{ expanded }}
|
||||||
|
hitSlop={10}
|
||||||
|
onPress={() => setExpanded((value) => !value)}
|
||||||
|
>
|
||||||
|
<Text style={[muted, { fontWeight: "600" }]}>
|
||||||
|
{block.steps === 1 ? t("1 step") : t("{count} steps", { count: block.steps })}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
{latest && !expanded ? (
|
||||||
|
<Text style={muted} numberOfLines={1}>
|
||||||
|
· {t("last: {latest}", { latest })}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
{open && block.event === "autopilot.paused" ? (
|
{open && block.event === "autopilot.paused" ? (
|
||||||
<>
|
<>
|
||||||
<Text style={muted}>·</Text>
|
<Text style={muted}>·</Text>
|
||||||
@ -115,10 +145,38 @@ export function AutopilotLine({
|
|||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
{expanded && checkpoints.length > 0 ? (
|
||||||
|
<View style={{ width: "100%", maxWidth: 420, gap: 4, paddingHorizontal: 16 }}>
|
||||||
|
{checkpoints.map((checkpoint, index) => (
|
||||||
|
<View key={`${checkpoint.at}-${index}`} style={{ flexDirection: "row", gap: 8 }}>
|
||||||
|
<Text
|
||||||
|
style={[muted, { width: 64, textAlign: "right", fontVariant: ["tabular-nums"] }]}
|
||||||
|
>
|
||||||
|
{time.format(new Date(checkpoint.at))}
|
||||||
|
</Text>
|
||||||
|
<Text style={{ flex: 1, color: tokens.foreground, fontSize: 13.5 }}>
|
||||||
|
{checkpoint.text}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
{block.event === "autopilot.finished" && block.text ? (
|
{block.event === "autopilot.finished" && block.text ? (
|
||||||
<Text style={{ color: tokens.foreground, fontSize: 14, textAlign: "center" }}>
|
<View
|
||||||
{block.text}
|
style={{
|
||||||
</Text>
|
width: "100%",
|
||||||
|
maxWidth: 520,
|
||||||
|
marginTop: 4,
|
||||||
|
borderRadius: 12,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: tokens.border,
|
||||||
|
backgroundColor: tokens.card,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
paddingVertical: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ color: tokens.foreground, fontSize: 14 }}>{block.text}</Text>
|
||||||
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -691,6 +691,12 @@ export const RU_MESSAGES: Record<string, string> = {
|
|||||||
Expired: "Истекло",
|
Expired: "Истекло",
|
||||||
Autopilot: "Автопилот",
|
Autopilot: "Автопилот",
|
||||||
"Autopilot finished": "Автопилот завершён",
|
"Autopilot finished": "Автопилот завершён",
|
||||||
|
"Until when?": "До какого времени?",
|
||||||
|
"Until done": "До готовности",
|
||||||
|
"Until tomorrow morning": "До завтрашнего утра",
|
||||||
|
"1 step": "1 шаг",
|
||||||
|
"{count} steps": "Шагов: {count}",
|
||||||
|
"last: {latest}": "последний: {latest}",
|
||||||
"Autopilot paused": "Автопилот на паузе",
|
"Autopilot paused": "Автопилот на паузе",
|
||||||
"Autopilot stopped": "Автопилот остановлен",
|
"Autopilot stopped": "Автопилот остановлен",
|
||||||
"allowed: {allowed}": "разрешено: {allowed}",
|
"allowed: {allowed}": "разрешено: {allowed}",
|
||||||
|
|||||||
@ -675,6 +675,12 @@ export const ZH_MESSAGES: Record<string, string> = {
|
|||||||
Expired: "已过期",
|
Expired: "已过期",
|
||||||
Autopilot: "自动驾驶",
|
Autopilot: "自动驾驶",
|
||||||
"Autopilot finished": "自动驾驶已完成",
|
"Autopilot finished": "自动驾驶已完成",
|
||||||
|
"Until when?": "持续到什么时候?",
|
||||||
|
"Until done": "直到完成",
|
||||||
|
"Until tomorrow morning": "直到明天早上",
|
||||||
|
"1 step": "1 步",
|
||||||
|
"{count} steps": "{count} 步",
|
||||||
|
"last: {latest}": "最近:{latest}",
|
||||||
"Autopilot paused": "自动驾驶已暂停",
|
"Autopilot paused": "自动驾驶已暂停",
|
||||||
"Autopilot stopped": "自动驾驶已停止",
|
"Autopilot stopped": "自动驾驶已停止",
|
||||||
"allowed: {allowed}": "已允许:{allowed}",
|
"allowed: {allowed}": "已允许:{allowed}",
|
||||||
|
|||||||
@ -60,3 +60,51 @@ test("an autopilot without progress pauses and stops on request", async ({ page
|
|||||||
await stopped.scrollIntoViewIfNeeded();
|
await stopped.scrollIntoViewIfNeeded();
|
||||||
await captureScreenshot(page, testInfo, "autopilot-paused-light");
|
await captureScreenshot(page, testInfo, "autopilot-paused-light");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("an autopilot started from the composer works overnight and reports in Today", async ({
|
||||||
|
page,
|
||||||
|
}, testInfo) => {
|
||||||
|
const stamp = Date.now();
|
||||||
|
await signup(page, `autopilot-night-${stamp}@rakazo.test`, "password12", "Autopilot Night");
|
||||||
|
await completeOnboarding(page);
|
||||||
|
|
||||||
|
await page.getByPlaceholder(/Message/).fill("Tidy the tickets");
|
||||||
|
await page.getByRole("button", { name: "Add" }).click();
|
||||||
|
await page.getByRole("menuitem", { name: "Autopilot" }).click();
|
||||||
|
const dialog = page.getByTestId("autopilot-dialog");
|
||||||
|
await expect(dialog.getByRole("textbox", { name: "Order" })).toHaveValue("Tidy the tickets");
|
||||||
|
await dialog.getByRole("radio", { name: "Until tomorrow morning" }).click();
|
||||||
|
await dialog.getByRole("button", { name: "Start" }).click();
|
||||||
|
await expect(dialog).toBeHidden();
|
||||||
|
await expect(page.getByPlaceholder(/Message/)).toHaveValue("");
|
||||||
|
|
||||||
|
const start = page.locator('[data-testid="autopilot-line"][data-event="autopilot.started"]');
|
||||||
|
await expect(start).toContainText("Tidy the tickets", { timeout: 60_000 });
|
||||||
|
await expect(start).toContainText(/until 8:00/);
|
||||||
|
const finished = page.locator('[data-testid="autopilot-line"][data-event="autopilot.finished"]');
|
||||||
|
await expect(finished).toBeVisible({ timeout: 120_000 });
|
||||||
|
await expect(finished.getByTestId("autopilot-result")).toHaveText(
|
||||||
|
"Tidied 5 tickets; nothing waits on you.",
|
||||||
|
);
|
||||||
|
await start.getByRole("button", { name: "5 steps" }).click();
|
||||||
|
await expect(start.getByTestId("autopilot-steps").locator("li")).toHaveCount(5);
|
||||||
|
await expect(start.getByTestId("autopilot-steps")).toContainText("Ticket 5 tidied");
|
||||||
|
|
||||||
|
await useAppearance(page, "light");
|
||||||
|
await start.scrollIntoViewIfNeeded();
|
||||||
|
await captureScreenshot(page, testInfo, "autopilot-night-light");
|
||||||
|
await useAppearance(page, "dark");
|
||||||
|
await start.scrollIntoViewIfNeeded();
|
||||||
|
await captureScreenshot(page, testInfo, "autopilot-night-dark");
|
||||||
|
|
||||||
|
// The morning summary: what the autopilot did, in Today.
|
||||||
|
await page.getByRole("button", { name: "Today", exact: true }).click();
|
||||||
|
const row = page.getByTestId("heute-autopilot-row");
|
||||||
|
await expect(row).toHaveAttribute("data-status", "finished", { timeout: 30_000 });
|
||||||
|
await expect(row).toContainText("Tidy the tickets");
|
||||||
|
await expect(row).toContainText("Tidied 5 tickets; nothing waits on you.");
|
||||||
|
await expect(row).toContainText("5 steps");
|
||||||
|
await captureScreenshot(page, testInfo, "autopilot-today-dark");
|
||||||
|
await row.click();
|
||||||
|
await expect(start).toBeVisible();
|
||||||
|
});
|
||||||
|
|||||||
@ -196,7 +196,7 @@ test("chat composer controls are vertically centered", async ({ page }) => {
|
|||||||
await completeOnboarding(page);
|
await completeOnboarding(page);
|
||||||
|
|
||||||
const centers = await page.getByTestId("composer-bar").evaluate((composer) =>
|
const centers = await page.getByTestId("composer-bar").evaluate((composer) =>
|
||||||
["Attach file", "Message", "Voice", "Send"].map((label) => {
|
["Add", "Message", "Voice", "Send"].map((label) => {
|
||||||
const element = composer.querySelector<HTMLElement>(`[aria-label="${label}"]`);
|
const element = composer.querySelector<HTMLElement>(`[aria-label="${label}"]`);
|
||||||
if (!element) throw new Error(`Missing composer control: ${label}`);
|
if (!element) throw new Error(`Missing composer control: ${label}`);
|
||||||
const box = element.getBoundingClientRect();
|
const box = element.getBoundingClientRect();
|
||||||
|
|||||||
@ -645,5 +645,13 @@
|
|||||||
"Autopilot stopped": "Autopilot gestoppt",
|
"Autopilot stopped": "Autopilot gestoppt",
|
||||||
"Autopilot finished": "Autopilot beendet",
|
"Autopilot finished": "Autopilot beendet",
|
||||||
"until {until}": "bis {until}",
|
"until {until}": "bis {until}",
|
||||||
"allowed: {allowed}": "erlaubt: {allowed}"
|
"allowed: {allowed}": "erlaubt: {allowed}",
|
||||||
|
"{steps, plural, one {# step} other {# steps}}": "{steps, plural, one {# Schritt} other {# Schritte}}",
|
||||||
|
"last: {latest}": "zuletzt: {latest}",
|
||||||
|
"Order": "Auftrag",
|
||||||
|
"Time": "Uhrzeit",
|
||||||
|
"Until a time": "Bis zu einer Uhrzeit",
|
||||||
|
"Until done": "Bis fertig",
|
||||||
|
"Until tomorrow morning": "Bis morgen früh",
|
||||||
|
"Until when?": "Bis wann?"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -134,6 +134,25 @@ describe("buildHeuteBrief", () => {
|
|||||||
expect(brief.done[0]?.run.runId).toBe("run-4");
|
expect(brief.done[0]?.run.runId).toBe("run-4");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("leaves an autopilot's step runs to its own row", () => {
|
||||||
|
const brief = buildHeuteBrief({
|
||||||
|
bots: [],
|
||||||
|
groups: [],
|
||||||
|
activeRuns: [run({ runId: "step-2", status: "running", trigger: "autopilot" })],
|
||||||
|
recentRuns: [
|
||||||
|
run({
|
||||||
|
runId: "step-1",
|
||||||
|
status: "completed",
|
||||||
|
trigger: "autopilot",
|
||||||
|
updatedAt: "2026-09-22T09:00:00.000Z",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
now,
|
||||||
|
});
|
||||||
|
expect(brief.doing).toHaveLength(0);
|
||||||
|
expect(brief.done).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
it("caps needs-you at three items", () => {
|
it("caps needs-you at three items", () => {
|
||||||
const bots = Array.from({ length: 5 }, (_, index) =>
|
const bots = Array.from({ length: 5 }, (_, index) =>
|
||||||
bot({ id: `bot-${index}`, unread: true, name: `Bot ${index}` }),
|
bot({ id: `bot-${index}`, unread: true, name: `Bot ${index}` }),
|
||||||
|
|||||||
@ -117,12 +117,16 @@ export function buildHeuteBrief(params: {
|
|||||||
coveredTargets.add(key);
|
coveredTargets.add(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An autopilot's runs show as its one row in Today, not one row per step.
|
||||||
const doing: DoingItem[] = activeRuns
|
const doing: DoingItem[] = activeRuns
|
||||||
.filter((run) => isDoingStatus(run.status))
|
.filter((run) => isDoingStatus(run.status) && run.trigger !== "autopilot")
|
||||||
.map((run) => ({ kind: "run" as const, run }));
|
.map((run) => ({ kind: "run" as const, run }));
|
||||||
|
|
||||||
const done: DoneItem[] = recentRuns
|
const done: DoneItem[] = recentRuns
|
||||||
.filter((run) => run.status === "completed" && isToday(run.updatedAt, now))
|
.filter(
|
||||||
|
(run) =>
|
||||||
|
run.status === "completed" && run.trigger !== "autopilot" && isToday(run.updatedAt, now),
|
||||||
|
)
|
||||||
.slice(0, 5)
|
.slice(0, 5)
|
||||||
.map((run) => ({ kind: "run" as const, run }));
|
.map((run) => ({ kind: "run" as const, run }));
|
||||||
|
|
||||||
|
|||||||
@ -264,6 +264,11 @@ msgstr "Noch {remaining} · Bot beobachtet nur"
|
|||||||
msgid "{size} B"
|
msgid "{size} B"
|
||||||
msgstr "{size} B"
|
msgstr "{size} B"
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
msgid "{steps, plural, one {# step} other {# steps}}"
|
||||||
|
msgstr "{steps, plural, one {# Schritt} other {# Schritte}}"
|
||||||
|
|
||||||
#. placeholder {0}: runActivityLabel(run.status)
|
#. placeholder {0}: runActivityLabel(run.status)
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
msgid "{title}, {0}"
|
msgid "{title}, {0}"
|
||||||
@ -366,6 +371,7 @@ msgstr "Aktivität"
|
|||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
msgid "Add"
|
msgid "Add"
|
||||||
msgstr "Hinzufügen"
|
msgstr "Hinzufügen"
|
||||||
|
|
||||||
@ -764,7 +770,9 @@ msgstr "Automatische Absender"
|
|||||||
msgid "Automated senders will appear after they post here."
|
msgid "Automated senders will appear after they post here."
|
||||||
msgstr "Automatische Absender erscheinen, sobald sie hier etwas posten."
|
msgstr "Automatische Absender erscheinen, sobald sie hier etwas posten."
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/shell/autopilot-line.tsx
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
msgid "Autopilot"
|
msgid "Autopilot"
|
||||||
msgstr "Autopilot"
|
msgstr "Autopilot"
|
||||||
|
|
||||||
@ -2898,6 +2906,11 @@ msgstr "Sprache"
|
|||||||
msgid "Lashes"
|
msgid "Lashes"
|
||||||
msgstr "Wimpern"
|
msgstr "Wimpern"
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
msgid "last: {latest}"
|
||||||
|
msgstr "zuletzt: {latest}"
|
||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
msgid "Later"
|
msgid "Later"
|
||||||
msgstr "Später"
|
msgstr "Später"
|
||||||
@ -3743,6 +3756,10 @@ msgstr "Optionaler Header-Wert"
|
|||||||
msgid "Or connect an API key"
|
msgid "Or connect an API key"
|
||||||
msgstr "Oder einen API-Schlüssel verbinden"
|
msgstr "Oder einen API-Schlüssel verbinden"
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Order"
|
||||||
|
msgstr "Auftrag"
|
||||||
|
|
||||||
#: src/pages/memory-providers/SupermemorySettingsForm.tsx
|
#: src/pages/memory-providers/SupermemorySettingsForm.tsx
|
||||||
msgid "Organization API key"
|
msgid "Organization API key"
|
||||||
msgstr "Organisations-API-Schlüssel"
|
msgstr "Organisations-API-Schlüssel"
|
||||||
@ -3803,6 +3820,7 @@ msgstr "Füge die OpenAI-kompatible Adresse deines Servers ein. Rakazo ergänzt
|
|||||||
msgid "Paste your API key"
|
msgid "Paste your API key"
|
||||||
msgstr "Füge deinen API-Schlüssel ein"
|
msgstr "Füge deinen API-Schlüssel ein"
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "Paused"
|
msgid "Paused"
|
||||||
msgstr "Pausiert"
|
msgstr "Pausiert"
|
||||||
@ -4760,6 +4778,7 @@ msgid "Star"
|
|||||||
msgstr "Stern"
|
msgstr "Stern"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
msgid "Start"
|
msgid "Start"
|
||||||
msgstr "Starten"
|
msgstr "Starten"
|
||||||
|
|
||||||
@ -5042,6 +5061,10 @@ msgstr "Stufen"
|
|||||||
msgid "Tightening the copy…"
|
msgid "Tightening the copy…"
|
||||||
msgstr "Straffe den Text…"
|
msgstr "Straffe den Text…"
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Time"
|
||||||
|
msgstr "Uhrzeit"
|
||||||
|
|
||||||
#: src/pages/RoutineSchedule.tsx
|
#: src/pages/RoutineSchedule.tsx
|
||||||
msgid "Time of day"
|
msgid "Time of day"
|
||||||
msgstr "Uhrzeit"
|
msgstr "Uhrzeit"
|
||||||
@ -5109,6 +5132,7 @@ msgid "Triage in progress…"
|
|||||||
msgstr "Triage läuft…"
|
msgstr "Triage läuft…"
|
||||||
|
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
#: src/pages/shell/autopilot-line.tsx
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
#: src/pages/shell/routine-event-line.tsx
|
#: src/pages/shell/routine-event-line.tsx
|
||||||
msgid "Try again"
|
msgid "Try again"
|
||||||
@ -5163,6 +5187,22 @@ msgstr "Nicht unterstützter Dateityp: {0}"
|
|||||||
msgid "until {until}"
|
msgid "until {until}"
|
||||||
msgstr "bis {until}"
|
msgstr "bis {until}"
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until a time"
|
||||||
|
msgstr "Bis zu einer Uhrzeit"
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until done"
|
||||||
|
msgstr "Bis fertig"
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until tomorrow morning"
|
||||||
|
msgstr "Bis morgen früh"
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until when?"
|
||||||
|
msgstr "Bis wann?"
|
||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
#: src/components/SoftwareUpdateSection.tsx
|
#: src/components/SoftwareUpdateSection.tsx
|
||||||
msgid "Up to date"
|
msgid "Up to date"
|
||||||
|
|||||||
@ -264,6 +264,11 @@ msgstr "{remaining} left · bot is watching, not acting"
|
|||||||
msgid "{size} B"
|
msgid "{size} B"
|
||||||
msgstr "{size} B"
|
msgstr "{size} B"
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
msgid "{steps, plural, one {# step} other {# steps}}"
|
||||||
|
msgstr "{steps, plural, one {# step} other {# steps}}"
|
||||||
|
|
||||||
#. placeholder {0}: runActivityLabel(run.status)
|
#. placeholder {0}: runActivityLabel(run.status)
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
msgid "{title}, {0}"
|
msgid "{title}, {0}"
|
||||||
@ -366,6 +371,7 @@ msgstr "Activity"
|
|||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
msgid "Add"
|
msgid "Add"
|
||||||
msgstr "Add"
|
msgstr "Add"
|
||||||
|
|
||||||
@ -764,7 +770,9 @@ msgstr "Automated senders"
|
|||||||
msgid "Automated senders will appear after they post here."
|
msgid "Automated senders will appear after they post here."
|
||||||
msgstr "Automated senders will appear after they post here."
|
msgstr "Automated senders will appear after they post here."
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/shell/autopilot-line.tsx
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
msgid "Autopilot"
|
msgid "Autopilot"
|
||||||
msgstr "Autopilot"
|
msgstr "Autopilot"
|
||||||
|
|
||||||
@ -2898,6 +2906,11 @@ msgstr "Language"
|
|||||||
msgid "Lashes"
|
msgid "Lashes"
|
||||||
msgstr "Lashes"
|
msgstr "Lashes"
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
msgid "last: {latest}"
|
||||||
|
msgstr "last: {latest}"
|
||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
msgid "Later"
|
msgid "Later"
|
||||||
msgstr "Later"
|
msgstr "Later"
|
||||||
@ -3743,6 +3756,10 @@ msgstr "Optional header value"
|
|||||||
msgid "Or connect an API key"
|
msgid "Or connect an API key"
|
||||||
msgstr "Or connect an API key"
|
msgstr "Or connect an API key"
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Order"
|
||||||
|
msgstr "Order"
|
||||||
|
|
||||||
#: src/pages/memory-providers/SupermemorySettingsForm.tsx
|
#: src/pages/memory-providers/SupermemorySettingsForm.tsx
|
||||||
msgid "Organization API key"
|
msgid "Organization API key"
|
||||||
msgstr "Organization API key"
|
msgstr "Organization API key"
|
||||||
@ -3803,6 +3820,7 @@ msgstr "Paste the OpenAI-compatible address from your server. Rakazo adds /v1 if
|
|||||||
msgid "Paste your API key"
|
msgid "Paste your API key"
|
||||||
msgstr "Paste your API key"
|
msgstr "Paste your API key"
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "Paused"
|
msgid "Paused"
|
||||||
msgstr "Paused"
|
msgstr "Paused"
|
||||||
@ -4760,6 +4778,7 @@ msgid "Star"
|
|||||||
msgstr "Star"
|
msgstr "Star"
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
msgid "Start"
|
msgid "Start"
|
||||||
msgstr "Start"
|
msgstr "Start"
|
||||||
|
|
||||||
@ -5042,6 +5061,10 @@ msgstr "Tiers"
|
|||||||
msgid "Tightening the copy…"
|
msgid "Tightening the copy…"
|
||||||
msgstr "Tightening the copy…"
|
msgstr "Tightening the copy…"
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Time"
|
||||||
|
msgstr "Time"
|
||||||
|
|
||||||
#: src/pages/RoutineSchedule.tsx
|
#: src/pages/RoutineSchedule.tsx
|
||||||
msgid "Time of day"
|
msgid "Time of day"
|
||||||
msgstr "Time of day"
|
msgstr "Time of day"
|
||||||
@ -5109,6 +5132,7 @@ msgid "Triage in progress…"
|
|||||||
msgstr "Triage in progress…"
|
msgstr "Triage in progress…"
|
||||||
|
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
#: src/pages/shell/autopilot-line.tsx
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
#: src/pages/shell/routine-event-line.tsx
|
#: src/pages/shell/routine-event-line.tsx
|
||||||
msgid "Try again"
|
msgid "Try again"
|
||||||
@ -5163,6 +5187,22 @@ msgstr "Unsupported file type: {0}"
|
|||||||
msgid "until {until}"
|
msgid "until {until}"
|
||||||
msgstr "until {until}"
|
msgstr "until {until}"
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until a time"
|
||||||
|
msgstr "Until a time"
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until done"
|
||||||
|
msgstr "Until done"
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until tomorrow morning"
|
||||||
|
msgstr "Until tomorrow morning"
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until when?"
|
||||||
|
msgstr "Until when?"
|
||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
#: src/components/SoftwareUpdateSection.tsx
|
#: src/components/SoftwareUpdateSection.tsx
|
||||||
msgid "Up to date"
|
msgid "Up to date"
|
||||||
|
|||||||
@ -264,6 +264,11 @@ msgstr "Quedan {remaining} · el bot observa, no actúa"
|
|||||||
msgid "{size} B"
|
msgid "{size} B"
|
||||||
msgstr "{size} B"
|
msgstr "{size} B"
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
msgid "{steps, plural, one {# step} other {# steps}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: runActivityLabel(run.status)
|
#. placeholder {0}: runActivityLabel(run.status)
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
msgid "{title}, {0}"
|
msgid "{title}, {0}"
|
||||||
@ -366,6 +371,7 @@ msgstr "Actividad"
|
|||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
msgid "Add"
|
msgid "Add"
|
||||||
msgstr "Agregar"
|
msgstr "Agregar"
|
||||||
|
|
||||||
@ -764,7 +770,9 @@ msgstr ""
|
|||||||
msgid "Automated senders will appear after they post here."
|
msgid "Automated senders will appear after they post here."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/shell/autopilot-line.tsx
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
msgid "Autopilot"
|
msgid "Autopilot"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -2898,6 +2906,11 @@ msgstr "Idioma"
|
|||||||
msgid "Lashes"
|
msgid "Lashes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
msgid "last: {latest}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
msgid "Later"
|
msgid "Later"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3743,6 +3756,10 @@ msgstr "Valor de encabezado opcional"
|
|||||||
msgid "Or connect an API key"
|
msgid "Or connect an API key"
|
||||||
msgstr "O conecta una clave de API"
|
msgstr "O conecta una clave de API"
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Order"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/memory-providers/SupermemorySettingsForm.tsx
|
#: src/pages/memory-providers/SupermemorySettingsForm.tsx
|
||||||
msgid "Organization API key"
|
msgid "Organization API key"
|
||||||
msgstr "Clave de API de organización"
|
msgstr "Clave de API de organización"
|
||||||
@ -3803,6 +3820,7 @@ msgstr "Pega la dirección compatible con OpenAI de tu servidor. Rakazo agrega /
|
|||||||
msgid "Paste your API key"
|
msgid "Paste your API key"
|
||||||
msgstr "Pega tu clave de API"
|
msgstr "Pega tu clave de API"
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "Paused"
|
msgid "Paused"
|
||||||
msgstr "En pausa"
|
msgstr "En pausa"
|
||||||
@ -4760,6 +4778,7 @@ msgid "Star"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
msgid "Start"
|
msgid "Start"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -5042,6 +5061,10 @@ msgstr ""
|
|||||||
msgid "Tightening the copy…"
|
msgid "Tightening the copy…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/RoutineSchedule.tsx
|
#: src/pages/RoutineSchedule.tsx
|
||||||
msgid "Time of day"
|
msgid "Time of day"
|
||||||
msgstr "Hora del día"
|
msgstr "Hora del día"
|
||||||
@ -5109,6 +5132,7 @@ msgid "Triage in progress…"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
#: src/pages/shell/autopilot-line.tsx
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
#: src/pages/shell/routine-event-line.tsx
|
#: src/pages/shell/routine-event-line.tsx
|
||||||
msgid "Try again"
|
msgid "Try again"
|
||||||
@ -5163,6 +5187,22 @@ msgstr "Tipo de archivo no compatible: {0}"
|
|||||||
msgid "until {until}"
|
msgid "until {until}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until a time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until done"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until tomorrow morning"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until when?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
#: src/components/SoftwareUpdateSection.tsx
|
#: src/components/SoftwareUpdateSection.tsx
|
||||||
msgid "Up to date"
|
msgid "Up to date"
|
||||||
|
|||||||
@ -264,6 +264,11 @@ msgstr "{remaining} शेष · बॉट देख रहा है, कार
|
|||||||
msgid "{size} B"
|
msgid "{size} B"
|
||||||
msgstr "{size} B"
|
msgstr "{size} B"
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
msgid "{steps, plural, one {# step} other {# steps}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: runActivityLabel(run.status)
|
#. placeholder {0}: runActivityLabel(run.status)
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
msgid "{title}, {0}"
|
msgid "{title}, {0}"
|
||||||
@ -366,6 +371,7 @@ msgstr "गतिविधि"
|
|||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
msgid "Add"
|
msgid "Add"
|
||||||
msgstr "जोड़ें"
|
msgstr "जोड़ें"
|
||||||
|
|
||||||
@ -764,7 +770,9 @@ msgstr ""
|
|||||||
msgid "Automated senders will appear after they post here."
|
msgid "Automated senders will appear after they post here."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/shell/autopilot-line.tsx
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
msgid "Autopilot"
|
msgid "Autopilot"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -2898,6 +2906,11 @@ msgstr "भाषा"
|
|||||||
msgid "Lashes"
|
msgid "Lashes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
msgid "last: {latest}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
msgid "Later"
|
msgid "Later"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3743,6 +3756,10 @@ msgstr "वैकल्पिक हेडर मान"
|
|||||||
msgid "Or connect an API key"
|
msgid "Or connect an API key"
|
||||||
msgstr "या एक API कुंजी कनेक्ट करें"
|
msgstr "या एक API कुंजी कनेक्ट करें"
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Order"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/memory-providers/SupermemorySettingsForm.tsx
|
#: src/pages/memory-providers/SupermemorySettingsForm.tsx
|
||||||
msgid "Organization API key"
|
msgid "Organization API key"
|
||||||
msgstr "संगठन API कुंजी"
|
msgstr "संगठन API कुंजी"
|
||||||
@ -3803,6 +3820,7 @@ msgstr "अपने सर्वर से OpenAI-संगत पता पे
|
|||||||
msgid "Paste your API key"
|
msgid "Paste your API key"
|
||||||
msgstr "अपनी API कुंजी पेस्ट करें"
|
msgstr "अपनी API कुंजी पेस्ट करें"
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "Paused"
|
msgid "Paused"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -4760,6 +4778,7 @@ msgid "Star"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
msgid "Start"
|
msgid "Start"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -5042,6 +5061,10 @@ msgstr ""
|
|||||||
msgid "Tightening the copy…"
|
msgid "Tightening the copy…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/RoutineSchedule.tsx
|
#: src/pages/RoutineSchedule.tsx
|
||||||
msgid "Time of day"
|
msgid "Time of day"
|
||||||
msgstr "दिन का समय"
|
msgstr "दिन का समय"
|
||||||
@ -5109,6 +5132,7 @@ msgid "Triage in progress…"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
#: src/pages/shell/autopilot-line.tsx
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
#: src/pages/shell/routine-event-line.tsx
|
#: src/pages/shell/routine-event-line.tsx
|
||||||
msgid "Try again"
|
msgid "Try again"
|
||||||
@ -5163,6 +5187,22 @@ msgstr "असमर्थित फ़ाइल प्रकार: {0}"
|
|||||||
msgid "until {until}"
|
msgid "until {until}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until a time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until done"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until tomorrow morning"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until when?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
#: src/components/SoftwareUpdateSection.tsx
|
#: src/components/SoftwareUpdateSection.tsx
|
||||||
msgid "Up to date"
|
msgid "Up to date"
|
||||||
|
|||||||
@ -264,6 +264,11 @@ msgstr "남은 시간 {remaining} · Bot은 화면만 보고 직접 조작하지
|
|||||||
msgid "{size} B"
|
msgid "{size} B"
|
||||||
msgstr "{size} B"
|
msgstr "{size} B"
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
msgid "{steps, plural, one {# step} other {# steps}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: runActivityLabel(run.status)
|
#. placeholder {0}: runActivityLabel(run.status)
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
msgid "{title}, {0}"
|
msgid "{title}, {0}"
|
||||||
@ -366,6 +371,7 @@ msgstr "활동"
|
|||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
msgid "Add"
|
msgid "Add"
|
||||||
msgstr "추가"
|
msgstr "추가"
|
||||||
|
|
||||||
@ -764,7 +770,9 @@ msgstr ""
|
|||||||
msgid "Automated senders will appear after they post here."
|
msgid "Automated senders will appear after they post here."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/shell/autopilot-line.tsx
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
msgid "Autopilot"
|
msgid "Autopilot"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -2898,6 +2906,11 @@ msgstr "언어"
|
|||||||
msgid "Lashes"
|
msgid "Lashes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
msgid "last: {latest}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
msgid "Later"
|
msgid "Later"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3743,6 +3756,10 @@ msgstr "선택 사항인 헤더 값"
|
|||||||
msgid "Or connect an API key"
|
msgid "Or connect an API key"
|
||||||
msgstr "또는 API 키 연결"
|
msgstr "또는 API 키 연결"
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Order"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/memory-providers/SupermemorySettingsForm.tsx
|
#: src/pages/memory-providers/SupermemorySettingsForm.tsx
|
||||||
msgid "Organization API key"
|
msgid "Organization API key"
|
||||||
msgstr "조직 API 키"
|
msgstr "조직 API 키"
|
||||||
@ -3803,6 +3820,7 @@ msgstr "서버의 OpenAI 호환 주소를 붙여넣으세요. 필요하면 Rakaz
|
|||||||
msgid "Paste your API key"
|
msgid "Paste your API key"
|
||||||
msgstr "API 키를 붙여넣으세요"
|
msgstr "API 키를 붙여넣으세요"
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "Paused"
|
msgid "Paused"
|
||||||
msgstr "일시 중지됨"
|
msgstr "일시 중지됨"
|
||||||
@ -4760,6 +4778,7 @@ msgid "Star"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
msgid "Start"
|
msgid "Start"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -5042,6 +5061,10 @@ msgstr ""
|
|||||||
msgid "Tightening the copy…"
|
msgid "Tightening the copy…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/RoutineSchedule.tsx
|
#: src/pages/RoutineSchedule.tsx
|
||||||
msgid "Time of day"
|
msgid "Time of day"
|
||||||
msgstr "실행 시각"
|
msgstr "실행 시각"
|
||||||
@ -5109,6 +5132,7 @@ msgid "Triage in progress…"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
#: src/pages/shell/autopilot-line.tsx
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
#: src/pages/shell/routine-event-line.tsx
|
#: src/pages/shell/routine-event-line.tsx
|
||||||
msgid "Try again"
|
msgid "Try again"
|
||||||
@ -5163,6 +5187,22 @@ msgstr "지원하지 않는 파일 형식: {0}"
|
|||||||
msgid "until {until}"
|
msgid "until {until}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until a time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until done"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until tomorrow morning"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until when?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
#: src/components/SoftwareUpdateSection.tsx
|
#: src/components/SoftwareUpdateSection.tsx
|
||||||
msgid "Up to date"
|
msgid "Up to date"
|
||||||
|
|||||||
@ -264,6 +264,11 @@ msgstr "{remaining} restantes · o bot está observando, não agindo"
|
|||||||
msgid "{size} B"
|
msgid "{size} B"
|
||||||
msgstr "{size} B"
|
msgstr "{size} B"
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
msgid "{steps, plural, one {# step} other {# steps}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: runActivityLabel(run.status)
|
#. placeholder {0}: runActivityLabel(run.status)
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
msgid "{title}, {0}"
|
msgid "{title}, {0}"
|
||||||
@ -366,6 +371,7 @@ msgstr "Atividade"
|
|||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
msgid "Add"
|
msgid "Add"
|
||||||
msgstr "Adicionar"
|
msgstr "Adicionar"
|
||||||
|
|
||||||
@ -764,7 +770,9 @@ msgstr ""
|
|||||||
msgid "Automated senders will appear after they post here."
|
msgid "Automated senders will appear after they post here."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/shell/autopilot-line.tsx
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
msgid "Autopilot"
|
msgid "Autopilot"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -2898,6 +2906,11 @@ msgstr "Idioma"
|
|||||||
msgid "Lashes"
|
msgid "Lashes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
msgid "last: {latest}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
msgid "Later"
|
msgid "Later"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3743,6 +3756,10 @@ msgstr "Valor de cabeçalho opcional"
|
|||||||
msgid "Or connect an API key"
|
msgid "Or connect an API key"
|
||||||
msgstr "Ou conecte uma chave de API"
|
msgstr "Ou conecte uma chave de API"
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Order"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/memory-providers/SupermemorySettingsForm.tsx
|
#: src/pages/memory-providers/SupermemorySettingsForm.tsx
|
||||||
msgid "Organization API key"
|
msgid "Organization API key"
|
||||||
msgstr "Chave da API da organização"
|
msgstr "Chave da API da organização"
|
||||||
@ -3803,6 +3820,7 @@ msgstr "Cole o endereço compatível com OpenAI do seu servidor. Rakazo adiciona
|
|||||||
msgid "Paste your API key"
|
msgid "Paste your API key"
|
||||||
msgstr "Cole sua chave API"
|
msgstr "Cole sua chave API"
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "Paused"
|
msgid "Paused"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -4760,6 +4778,7 @@ msgid "Star"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
msgid "Start"
|
msgid "Start"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -5042,6 +5061,10 @@ msgstr ""
|
|||||||
msgid "Tightening the copy…"
|
msgid "Tightening the copy…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/RoutineSchedule.tsx
|
#: src/pages/RoutineSchedule.tsx
|
||||||
msgid "Time of day"
|
msgid "Time of day"
|
||||||
msgstr "Hora do dia"
|
msgstr "Hora do dia"
|
||||||
@ -5109,6 +5132,7 @@ msgid "Triage in progress…"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
#: src/pages/shell/autopilot-line.tsx
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
#: src/pages/shell/routine-event-line.tsx
|
#: src/pages/shell/routine-event-line.tsx
|
||||||
msgid "Try again"
|
msgid "Try again"
|
||||||
@ -5163,6 +5187,22 @@ msgstr "Tipo de arquivo não suportado: {0}"
|
|||||||
msgid "until {until}"
|
msgid "until {until}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until a time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until done"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until tomorrow morning"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until when?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
#: src/components/SoftwareUpdateSection.tsx
|
#: src/components/SoftwareUpdateSection.tsx
|
||||||
msgid "Up to date"
|
msgid "Up to date"
|
||||||
|
|||||||
@ -264,6 +264,11 @@ msgstr "Осталось: {remaining} · бот наблюдает, но не д
|
|||||||
msgid "{size} B"
|
msgid "{size} B"
|
||||||
msgstr "{size} Б"
|
msgstr "{size} Б"
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
msgid "{steps, plural, one {# step} other {# steps}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: runActivityLabel(run.status)
|
#. placeholder {0}: runActivityLabel(run.status)
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
msgid "{title}, {0}"
|
msgid "{title}, {0}"
|
||||||
@ -366,6 +371,7 @@ msgstr "Активность"
|
|||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
msgid "Add"
|
msgid "Add"
|
||||||
msgstr "Добавить"
|
msgstr "Добавить"
|
||||||
|
|
||||||
@ -764,7 +770,9 @@ msgstr "Автоматические отправители"
|
|||||||
msgid "Automated senders will appear after they post here."
|
msgid "Automated senders will appear after they post here."
|
||||||
msgstr "Автоматические отправители появятся после публикации здесь."
|
msgstr "Автоматические отправители появятся после публикации здесь."
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/shell/autopilot-line.tsx
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
msgid "Autopilot"
|
msgid "Autopilot"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -2898,6 +2906,11 @@ msgstr "Язык"
|
|||||||
msgid "Lashes"
|
msgid "Lashes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
msgid "last: {latest}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
msgid "Later"
|
msgid "Later"
|
||||||
msgstr "Позже"
|
msgstr "Позже"
|
||||||
@ -3743,6 +3756,10 @@ msgstr "Необязательное значение заголовка"
|
|||||||
msgid "Or connect an API key"
|
msgid "Or connect an API key"
|
||||||
msgstr "Или подключить API-ключ"
|
msgstr "Или подключить API-ключ"
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Order"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/memory-providers/SupermemorySettingsForm.tsx
|
#: src/pages/memory-providers/SupermemorySettingsForm.tsx
|
||||||
msgid "Organization API key"
|
msgid "Organization API key"
|
||||||
msgstr "Ключ API организации"
|
msgstr "Ключ API организации"
|
||||||
@ -3803,6 +3820,7 @@ msgstr "Вставьте OpenAI-совместимый адрес со свое
|
|||||||
msgid "Paste your API key"
|
msgid "Paste your API key"
|
||||||
msgstr "Вставьте свой ключ API"
|
msgstr "Вставьте свой ключ API"
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "Paused"
|
msgid "Paused"
|
||||||
msgstr "Приостановлено"
|
msgstr "Приостановлено"
|
||||||
@ -4760,6 +4778,7 @@ msgid "Star"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
msgid "Start"
|
msgid "Start"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -5042,6 +5061,10 @@ msgstr ""
|
|||||||
msgid "Tightening the copy…"
|
msgid "Tightening the copy…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/RoutineSchedule.tsx
|
#: src/pages/RoutineSchedule.tsx
|
||||||
msgid "Time of day"
|
msgid "Time of day"
|
||||||
msgstr "Время суток"
|
msgstr "Время суток"
|
||||||
@ -5109,6 +5132,7 @@ msgid "Triage in progress…"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
#: src/pages/shell/autopilot-line.tsx
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
#: src/pages/shell/routine-event-line.tsx
|
#: src/pages/shell/routine-event-line.tsx
|
||||||
msgid "Try again"
|
msgid "Try again"
|
||||||
@ -5163,6 +5187,22 @@ msgstr "Неподдерживаемый тип файла: {0}."
|
|||||||
msgid "until {until}"
|
msgid "until {until}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until a time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until done"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until tomorrow morning"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until when?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
#: src/components/SoftwareUpdateSection.tsx
|
#: src/components/SoftwareUpdateSection.tsx
|
||||||
msgid "Up to date"
|
msgid "Up to date"
|
||||||
|
|||||||
@ -264,6 +264,11 @@ msgstr "{remaining} kaldı · bot yalnızca izliyor"
|
|||||||
msgid "{size} B"
|
msgid "{size} B"
|
||||||
msgstr "{size} B"
|
msgstr "{size} B"
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
msgid "{steps, plural, one {# step} other {# steps}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: runActivityLabel(run.status)
|
#. placeholder {0}: runActivityLabel(run.status)
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
msgid "{title}, {0}"
|
msgid "{title}, {0}"
|
||||||
@ -366,6 +371,7 @@ msgstr "Etkinlik"
|
|||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
msgid "Add"
|
msgid "Add"
|
||||||
msgstr "Ekle"
|
msgstr "Ekle"
|
||||||
|
|
||||||
@ -764,7 +770,9 @@ msgstr ""
|
|||||||
msgid "Automated senders will appear after they post here."
|
msgid "Automated senders will appear after they post here."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/shell/autopilot-line.tsx
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
msgid "Autopilot"
|
msgid "Autopilot"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -2898,6 +2906,11 @@ msgstr "Dil"
|
|||||||
msgid "Lashes"
|
msgid "Lashes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
msgid "last: {latest}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
msgid "Later"
|
msgid "Later"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3743,6 +3756,10 @@ msgstr "İsteğe bağlı üstbilgi değeri"
|
|||||||
msgid "Or connect an API key"
|
msgid "Or connect an API key"
|
||||||
msgstr "Veya bir API anahtarı bağla"
|
msgstr "Veya bir API anahtarı bağla"
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Order"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/memory-providers/SupermemorySettingsForm.tsx
|
#: src/pages/memory-providers/SupermemorySettingsForm.tsx
|
||||||
msgid "Organization API key"
|
msgid "Organization API key"
|
||||||
msgstr "Kuruluş API anahtarı"
|
msgstr "Kuruluş API anahtarı"
|
||||||
@ -3803,6 +3820,7 @@ msgstr "Sunucundaki OpenAI uyumlu adresi yapıştır. Gerekirse Rakazo /v1 ekler
|
|||||||
msgid "Paste your API key"
|
msgid "Paste your API key"
|
||||||
msgstr "API anahtarını yapıştır"
|
msgstr "API anahtarını yapıştır"
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "Paused"
|
msgid "Paused"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -4760,6 +4778,7 @@ msgid "Star"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
msgid "Start"
|
msgid "Start"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -5042,6 +5061,10 @@ msgstr ""
|
|||||||
msgid "Tightening the copy…"
|
msgid "Tightening the copy…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/RoutineSchedule.tsx
|
#: src/pages/RoutineSchedule.tsx
|
||||||
msgid "Time of day"
|
msgid "Time of day"
|
||||||
msgstr "Günün saati"
|
msgstr "Günün saati"
|
||||||
@ -5109,6 +5132,7 @@ msgid "Triage in progress…"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
#: src/pages/shell/autopilot-line.tsx
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
#: src/pages/shell/routine-event-line.tsx
|
#: src/pages/shell/routine-event-line.tsx
|
||||||
msgid "Try again"
|
msgid "Try again"
|
||||||
@ -5163,6 +5187,22 @@ msgstr "Desteklenmeyen dosya türü: {0}"
|
|||||||
msgid "until {until}"
|
msgid "until {until}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until a time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until done"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until tomorrow morning"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until when?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
#: src/components/SoftwareUpdateSection.tsx
|
#: src/components/SoftwareUpdateSection.tsx
|
||||||
msgid "Up to date"
|
msgid "Up to date"
|
||||||
|
|||||||
@ -264,6 +264,11 @@ msgstr "剩余 {remaining} · Bot 只看不操作"
|
|||||||
msgid "{size} B"
|
msgid "{size} B"
|
||||||
msgstr "{size} B"
|
msgstr "{size} B"
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
msgid "{steps, plural, one {# step} other {# steps}}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#. placeholder {0}: runActivityLabel(run.status)
|
#. placeholder {0}: runActivityLabel(run.status)
|
||||||
#: src/pages/HeuteBrief.tsx
|
#: src/pages/HeuteBrief.tsx
|
||||||
msgid "{title}, {0}"
|
msgid "{title}, {0}"
|
||||||
@ -366,6 +371,7 @@ msgstr "动态"
|
|||||||
#: src/components/onboarding/OnboardingScanStep.tsx
|
#: src/components/onboarding/OnboardingScanStep.tsx
|
||||||
#: src/pages/PluginsOverlay.tsx
|
#: src/pages/PluginsOverlay.tsx
|
||||||
#: src/pages/ScratchpadSection.tsx
|
#: src/pages/ScratchpadSection.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
msgid "Add"
|
msgid "Add"
|
||||||
msgstr "添加"
|
msgstr "添加"
|
||||||
|
|
||||||
@ -764,7 +770,9 @@ msgstr ""
|
|||||||
msgid "Automated senders will appear after they post here."
|
msgid "Automated senders will appear after they post here."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/shell/autopilot-line.tsx
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
#: src/pages/shell/composer.tsx
|
||||||
msgid "Autopilot"
|
msgid "Autopilot"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -2898,6 +2906,11 @@ msgstr "语言"
|
|||||||
msgid "Lashes"
|
msgid "Lashes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
|
msgid "last: {latest}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
msgid "Later"
|
msgid "Later"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -3743,6 +3756,10 @@ msgstr "可选请求头值"
|
|||||||
msgid "Or connect an API key"
|
msgid "Or connect an API key"
|
||||||
msgstr "或连接 API 密钥"
|
msgstr "或连接 API 密钥"
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Order"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/memory-providers/SupermemorySettingsForm.tsx
|
#: src/pages/memory-providers/SupermemorySettingsForm.tsx
|
||||||
msgid "Organization API key"
|
msgid "Organization API key"
|
||||||
msgstr "组织 API 密钥"
|
msgstr "组织 API 密钥"
|
||||||
@ -3803,6 +3820,7 @@ msgstr "粘贴服务器的 OpenAI 兼容地址。需要时 Rakazo 会补上 /v1
|
|||||||
msgid "Paste your API key"
|
msgid "Paste your API key"
|
||||||
msgstr "粘贴你的 API 密钥"
|
msgstr "粘贴你的 API 密钥"
|
||||||
|
|
||||||
|
#: src/pages/HeuteBrief.tsx
|
||||||
#: src/pages/RoutineEditor.tsx
|
#: src/pages/RoutineEditor.tsx
|
||||||
msgid "Paused"
|
msgid "Paused"
|
||||||
msgstr "已暂停"
|
msgstr "已暂停"
|
||||||
@ -4760,6 +4778,7 @@ msgid "Star"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Shell.tsx
|
#: src/pages/Shell.tsx
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
msgid "Start"
|
msgid "Start"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -5042,6 +5061,10 @@ msgstr ""
|
|||||||
msgid "Tightening the copy…"
|
msgid "Tightening the copy…"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/RoutineSchedule.tsx
|
#: src/pages/RoutineSchedule.tsx
|
||||||
msgid "Time of day"
|
msgid "Time of day"
|
||||||
msgstr "时间"
|
msgstr "时间"
|
||||||
@ -5109,6 +5132,7 @@ msgid "Triage in progress…"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/pages/Onboarding.tsx
|
#: src/pages/Onboarding.tsx
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
#: src/pages/shell/autopilot-line.tsx
|
#: src/pages/shell/autopilot-line.tsx
|
||||||
#: src/pages/shell/routine-event-line.tsx
|
#: src/pages/shell/routine-event-line.tsx
|
||||||
msgid "Try again"
|
msgid "Try again"
|
||||||
@ -5163,6 +5187,22 @@ msgstr "不支持的文件类型:{0}"
|
|||||||
msgid "until {until}"
|
msgid "until {until}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until a time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until done"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until tomorrow morning"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/pages/shell/autopilot-dialog.tsx
|
||||||
|
msgid "Until when?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/DesktopUpdates.tsx
|
#: src/components/DesktopUpdates.tsx
|
||||||
#: src/components/SoftwareUpdateSection.tsx
|
#: src/components/SoftwareUpdateSection.tsx
|
||||||
msgid "Up to date"
|
msgid "Up to date"
|
||||||
|
|||||||
@ -1,8 +1,16 @@
|
|||||||
import { i18n } from "@lingui/core";
|
import { i18n } from "@lingui/core";
|
||||||
import { Plural, Trans, useLingui } from "@lingui/react/macro";
|
import { Plural, Trans, useLingui } from "@lingui/react/macro";
|
||||||
import type { Bot, CoachToday, Group, HabitToday, RunActivityRow } from "@rakazo/contracts";
|
import type {
|
||||||
|
AutopilotRecent,
|
||||||
|
Bot,
|
||||||
|
CoachToday,
|
||||||
|
Group,
|
||||||
|
HabitToday,
|
||||||
|
RunActivityRow,
|
||||||
|
} from "@rakazo/contracts";
|
||||||
import { botColors } from "@rakazo/ui-tokens";
|
import { botColors } from "@rakazo/ui-tokens";
|
||||||
import { BotAvatar, Button, EvooMascot, GroupAvatar } from "@rakazo/ui-web";
|
import { BotAvatar, Button, EvooMascot, GroupAvatar, Icon } from "@rakazo/ui-web";
|
||||||
|
import { CircleCheck, Pause, Play, Square } from "lucide-react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { CoachRhythmControl } from "../components/heute/CoachRhythmControl";
|
import { CoachRhythmControl } from "../components/heute/CoachRhythmControl";
|
||||||
import { HeuteCoach } from "../components/heute/HeuteCoach";
|
import { HeuteCoach } from "../components/heute/HeuteCoach";
|
||||||
@ -39,6 +47,7 @@ export function HeuteBrief({
|
|||||||
}: HeuteBriefProps) {
|
}: HeuteBriefProps) {
|
||||||
const [activeRuns, setActiveRuns] = useState<RunActivityRow[]>([]);
|
const [activeRuns, setActiveRuns] = useState<RunActivityRow[]>([]);
|
||||||
const [recentRuns, setRecentRuns] = useState<RunActivityRow[]>([]);
|
const [recentRuns, setRecentRuns] = useState<RunActivityRow[]>([]);
|
||||||
|
const [autopilots, setAutopilots] = useState<AutopilotRecent[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [coach, setCoach] = useState<CoachToday | null>(null);
|
const [coach, setCoach] = useState<CoachToday | null>(null);
|
||||||
const [coachSettled, setCoachSettled] = useState(false);
|
const [coachSettled, setCoachSettled] = useState(false);
|
||||||
@ -50,13 +59,15 @@ export function HeuteBrief({
|
|||||||
|
|
||||||
const tick = async () => {
|
const tick = async () => {
|
||||||
try {
|
try {
|
||||||
const [active, recent] = await Promise.all([
|
const [active, recent, autopilot] = await Promise.all([
|
||||||
rpc.runs.list({ filter: "active" }),
|
rpc.runs.list({ filter: "active" }),
|
||||||
rpc.runs.list({ filter: "recent" }),
|
rpc.runs.list({ filter: "recent" }),
|
||||||
|
rpc.autopilot.recent().catch(() => ({ items: [] })),
|
||||||
]);
|
]);
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setActiveRuns(active.runs);
|
setActiveRuns(active.runs);
|
||||||
setRecentRuns(recent.runs);
|
setRecentRuns(recent.runs);
|
||||||
|
setAutopilots(autopilot.items);
|
||||||
} catch {
|
} catch {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
} finally {
|
} finally {
|
||||||
@ -139,7 +150,11 @@ export function HeuteBrief({
|
|||||||
coach && (coach.brief || coach.habits.length > 0 || coach.goals.length > 0),
|
coach && (coach.brief || coach.habits.length > 0 || coach.goals.length > 0),
|
||||||
);
|
);
|
||||||
const hasContent =
|
const hasContent =
|
||||||
hasCoachContent || brief.needsYou.length > 0 || brief.doing.length > 0 || brief.done.length > 0;
|
hasCoachContent ||
|
||||||
|
autopilots.length > 0 ||
|
||||||
|
brief.needsYou.length > 0 ||
|
||||||
|
brief.doing.length > 0 ||
|
||||||
|
brief.done.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-0 flex-1 flex-col overflow-hidden" data-testid="heute-brief">
|
<div className="flex min-h-0 flex-1 flex-col overflow-hidden" data-testid="heute-brief">
|
||||||
@ -170,6 +185,27 @@ export function HeuteBrief({
|
|||||||
<div className="mx-auto flex w-full max-w-2xl flex-col gap-8 pb-4">
|
<div className="mx-auto flex w-full max-w-2xl flex-col gap-8 pb-4">
|
||||||
{coach ? <HeuteCoach today={coach} onHabitDone={setHabitDone} /> : null}
|
{coach ? <HeuteCoach today={coach} onHabitDone={setHabitDone} /> : null}
|
||||||
|
|
||||||
|
{autopilots.length > 0 ? (
|
||||||
|
<section data-testid="heute-autopilot">
|
||||||
|
<h2 className="rk-shell-section-label mb-2 px-0.5">
|
||||||
|
<Trans>Autopilot</Trans>
|
||||||
|
</h2>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{autopilots.map((autopilot) => (
|
||||||
|
<AutopilotRow
|
||||||
|
key={autopilot.id}
|
||||||
|
autopilot={autopilot}
|
||||||
|
onOpen={() =>
|
||||||
|
autopilot.groupId
|
||||||
|
? onOpenGroup(autopilot.groupId, autopilot.spaceId)
|
||||||
|
: onOpenBot(autopilot.botId, autopilot.spaceId)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{brief.needsYou.length > 0 ? (
|
{brief.needsYou.length > 0 ? (
|
||||||
<section data-testid="heute-needs-you">
|
<section data-testid="heute-needs-you">
|
||||||
<h2 className="rk-shell-section-label mb-2 px-0.5">
|
<h2 className="rk-shell-section-label mb-2 px-0.5">
|
||||||
@ -446,6 +482,55 @@ function DoneRow({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** One autopilot: what it works on, how far it got, and how it ended; opens its thread. */
|
||||||
|
function AutopilotRow({ autopilot, onOpen }: { autopilot: AutopilotRecent; onOpen: () => void }) {
|
||||||
|
const { t } = useLingui();
|
||||||
|
const steps = autopilot.stepCount;
|
||||||
|
const latest = autopilot.lastCheckpoint;
|
||||||
|
const icon = { active: Play, paused: Pause, finished: CircleCheck, stopped: Square }[
|
||||||
|
autopilot.status
|
||||||
|
];
|
||||||
|
const detail =
|
||||||
|
autopilot.status === "finished"
|
||||||
|
? autopilot.result
|
||||||
|
: autopilot.status === "paused"
|
||||||
|
? autopilot.pauseReason
|
||||||
|
: latest
|
||||||
|
? t`last: ${latest}`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-testid="heute-autopilot-row"
|
||||||
|
data-status={autopilot.status}
|
||||||
|
onClick={onOpen}
|
||||||
|
className="flex w-full items-start gap-3 rounded-2xl border border-transparent px-3 py-3 text-start transition-colors hover:border-border hover:bg-accent"
|
||||||
|
>
|
||||||
|
<span className="mt-0.5 flex size-10 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
||||||
|
<Icon icon={icon} size="sm" />
|
||||||
|
</span>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="truncate text-ui-body font-medium text-foreground" dir="auto">
|
||||||
|
{autopilot.goal}
|
||||||
|
</div>
|
||||||
|
{detail ? (
|
||||||
|
<div className="mt-0.5 line-clamp-3 text-ui-body-sm text-muted-foreground" dir="auto">
|
||||||
|
{detail}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{autopilot.status === "paused" ? (
|
||||||
|
<ActionableChip label={t`Paused`} />
|
||||||
|
) : (
|
||||||
|
<span className="shrink-0 text-ui-label text-muted-foreground">
|
||||||
|
<Plural value={steps} one="# step" other="# steps" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function jetztRowKey(item: JetztItem): string {
|
function jetztRowKey(item: JetztItem): string {
|
||||||
if (item.kind === "run") return `run:${item.run.runId}`;
|
if (item.kind === "run") return `run:${item.run.runId}`;
|
||||||
return `unread:${item.chatKind}:${item.chat.id}`;
|
return `unread:${item.chatKind}:${item.chat.id}`;
|
||||||
|
|||||||
@ -1993,6 +1993,18 @@ export function ShellPage() {
|
|||||||
await refreshThreadRef.current(botId);
|
await refreshThreadRef.current(botId);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
const startAutopilot = useCallback(async (goal: string, until?: string) => {
|
||||||
|
const botId = activeBotId.current;
|
||||||
|
const groupId = activeGroupId.current;
|
||||||
|
if (!botId && !groupId) return;
|
||||||
|
const target = groupId ? { groupId } : { botId: botId! };
|
||||||
|
await rpc.autopilot.start({ ...target, goal, ...(until ? { until } : {}) });
|
||||||
|
if (groupId && activeGroupId.current === groupId) {
|
||||||
|
await refreshGroupThreadRef.current(groupId);
|
||||||
|
} else if (botId && activeBotId.current === botId) {
|
||||||
|
await refreshThreadRef.current(botId);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
const reactToMessage = useCallback(
|
const reactToMessage = useCallback(
|
||||||
async (message: ThreadMessage, reaction: MessageReaction) => {
|
async (message: ThreadMessage, reaction: MessageReaction) => {
|
||||||
const botId = activeBotId.current;
|
const botId = activeBotId.current;
|
||||||
@ -3418,6 +3430,7 @@ export function ShellPage() {
|
|||||||
agentSkills={agentSkills}
|
agentSkills={agentSkills}
|
||||||
onSlashOpen={refreshAgentSkills}
|
onSlashOpen={refreshAgentSkills}
|
||||||
onConnectModel={bootstrapMe?.needsModel ? () => openSettings("models") : undefined}
|
onConnectModel={bootstrapMe?.needsModel ? () => openSettings("models") : undefined}
|
||||||
|
onAutopilot={startAutopilot}
|
||||||
onSlashAction={(action) => {
|
onSlashAction={(action) => {
|
||||||
if (action === "chat-settings") {
|
if (action === "chat-settings") {
|
||||||
setPanel(inGroup ? "group-settings" : "settings");
|
setPanel(inGroup ? "group-settings" : "settings");
|
||||||
|
|||||||
106
apps/web/src/pages/shell/autopilot-dialog.tsx
Normal file
106
apps/web/src/pages/shell/autopilot-dialog.tsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { useLingui } from "@lingui/react/macro";
|
||||||
|
import type { AutopilotUntilChoice } from "@rakazo/core";
|
||||||
|
import { autopilotUntil } from "@rakazo/core";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogTitle,
|
||||||
|
Input,
|
||||||
|
Textarea,
|
||||||
|
} from "@rakazo/ui-web";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
type Choice = AutopilotUntilChoice["kind"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the autopilot from the composer: the order (the draft so far) and until when it may
|
||||||
|
* work. Mounted only while open, so it always starts from the current draft.
|
||||||
|
*/
|
||||||
|
export function AutopilotDialog({
|
||||||
|
draft,
|
||||||
|
onClose,
|
||||||
|
onStart,
|
||||||
|
}: {
|
||||||
|
draft: string;
|
||||||
|
onClose: () => void;
|
||||||
|
onStart: (goal: string, until?: string) => Promise<void>;
|
||||||
|
}) {
|
||||||
|
const { t } = useLingui();
|
||||||
|
const [goal, setGoal] = useState(draft);
|
||||||
|
const [choice, setChoice] = useState<Choice>("done");
|
||||||
|
const [time, setTime] = useState("06:00");
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [failed, setFailed] = useState(false);
|
||||||
|
|
||||||
|
const options: { kind: Choice; label: string }[] = [
|
||||||
|
{ kind: "done", label: t`Until done` },
|
||||||
|
{ kind: "morning", label: t`Until tomorrow morning` },
|
||||||
|
{ kind: "time", label: t`Until a time` },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
const order = goal.trim();
|
||||||
|
if (!order) return;
|
||||||
|
setBusy(true);
|
||||||
|
setFailed(false);
|
||||||
|
try {
|
||||||
|
const until = autopilotUntil(
|
||||||
|
choice === "time" ? { kind: "time", time } : { kind: choice },
|
||||||
|
new Date(),
|
||||||
|
);
|
||||||
|
await onStart(order, until?.toISOString());
|
||||||
|
onClose();
|
||||||
|
} catch {
|
||||||
|
setFailed(true);
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open onOpenChange={(next) => !next && onClose()}>
|
||||||
|
<DialogContent data-testid="autopilot-dialog" className="sm:max-w-md">
|
||||||
|
<DialogTitle>{t`Until when?`}</DialogTitle>
|
||||||
|
<Textarea
|
||||||
|
aria-label={t`Order`}
|
||||||
|
value={goal}
|
||||||
|
maxLength={500}
|
||||||
|
rows={3}
|
||||||
|
onChange={(event) => setGoal(event.target.value)}
|
||||||
|
/>
|
||||||
|
<div role="radiogroup" aria-label={t`Until when?`} className="flex flex-wrap gap-2">
|
||||||
|
{options.map((option) => (
|
||||||
|
<Button
|
||||||
|
key={option.kind}
|
||||||
|
size="sm"
|
||||||
|
variant={choice === option.kind ? "default" : "outline"}
|
||||||
|
role="radio"
|
||||||
|
aria-checked={choice === option.kind}
|
||||||
|
onClick={() => setChoice(option.kind)}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
{choice === "time" ? (
|
||||||
|
<Input
|
||||||
|
type="time"
|
||||||
|
aria-label={t`Time`}
|
||||||
|
value={time}
|
||||||
|
onChange={(event) => {
|
||||||
|
if (event.target.value) setTime(event.target.value.slice(0, 5));
|
||||||
|
}}
|
||||||
|
className="h-control-sm w-32 px-2 tabular-nums"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button disabled={busy || !goal.trim()} onClick={() => void start()}>
|
||||||
|
{failed ? t`Try again` : t`Start`}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { useLingui } from "@lingui/react/macro";
|
import { Plural, useLingui } from "@lingui/react/macro";
|
||||||
import type { AutopilotBlock } from "@rakazo/contracts";
|
import type { AutopilotBlock } from "@rakazo/contracts";
|
||||||
import { formatAutopilotDuration } from "@rakazo/core";
|
import { formatAutopilotDuration } from "@rakazo/core";
|
||||||
import { Icon } from "@rakazo/ui-web";
|
import { Icon } from "@rakazo/ui-web";
|
||||||
import { CircleCheck, Pause, Play, Square } from "lucide-react";
|
import { ChevronDown, CircleCheck, Pause, Play, Square } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export type AutopilotAction = "stop" | "resume";
|
export type AutopilotAction = "stop" | "resume";
|
||||||
@ -11,8 +11,9 @@ const actionClass =
|
|||||||
"rounded-sm font-medium text-muted-foreground underline-offset-4 hover:text-foreground hover:underline focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50 disabled:opacity-50";
|
"rounded-sm font-medium text-muted-foreground underline-offset-4 hover:text-foreground hover:underline focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50 disabled:opacity-50";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One quiet line when the autopilot starts, pauses, stops or finishes. Stop stays on the start
|
* One quiet line when the autopilot starts, pauses, stops or finishes. The start line counts the
|
||||||
* line while it runs; a pause offers Continue.
|
* steps and folds open to the latest checkpoints; Stop stays on it while it runs, a pause offers
|
||||||
|
* Continue, and the result of a finished autopilot reads as a card.
|
||||||
*/
|
*/
|
||||||
export function AutopilotLine({
|
export function AutopilotLine({
|
||||||
block,
|
block,
|
||||||
@ -24,6 +25,7 @@ export function AutopilotLine({
|
|||||||
const { i18n, t } = useLingui();
|
const { i18n, t } = useLingui();
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [failed, setFailed] = useState(false);
|
const [failed, setFailed] = useState(false);
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
const open = !block.ended && Boolean(onAction);
|
const open = !block.ended && Boolean(onAction);
|
||||||
|
|
||||||
async function act(action: AutopilotAction) {
|
async function act(action: AutopilotAction) {
|
||||||
@ -44,6 +46,13 @@ export function AutopilotLine({
|
|||||||
new Date(block.until),
|
new Date(block.until),
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
const time = new Intl.DateTimeFormat(i18n.locale || "en", {
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
const checkpoints = block.event === "autopilot.started" ? (block.checkpoints ?? []) : [];
|
||||||
|
const latest = checkpoints.at(-1)?.text;
|
||||||
|
const steps = block.steps ?? 0;
|
||||||
const duration =
|
const duration =
|
||||||
block.durationMs !== undefined ? formatAutopilotDuration(block.durationMs) : null;
|
block.durationMs !== undefined ? formatAutopilotDuration(block.durationMs) : null;
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
@ -86,6 +95,30 @@ export function AutopilotLine({
|
|||||||
<span dir="auto">{part}</span>
|
<span dir="auto">{part}</span>
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
{steps > 0 && block.event === "autopilot.started" ? (
|
||||||
|
<>
|
||||||
|
<span aria-hidden>·</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`${actionClass} inline-flex items-center gap-0.5`}
|
||||||
|
aria-expanded={expanded}
|
||||||
|
onClick={() => setExpanded((value) => !value)}
|
||||||
|
>
|
||||||
|
<Plural value={steps} one="# step" other="# steps" />
|
||||||
|
<Icon
|
||||||
|
icon={ChevronDown}
|
||||||
|
size="sm"
|
||||||
|
className={expanded ? "rotate-180 transition-transform" : "transition-transform"}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{latest && !expanded ? (
|
||||||
|
<>
|
||||||
|
<span aria-hidden>·</span>
|
||||||
|
<span dir="auto" className="max-w-xs truncate">{t`last: ${latest}`}</span>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
{open && block.event === "autopilot.paused" ? (
|
{open && block.event === "autopilot.paused" ? (
|
||||||
<>
|
<>
|
||||||
<span aria-hidden>·</span>
|
<span aria-hidden>·</span>
|
||||||
@ -113,8 +146,29 @@ export function AutopilotLine({
|
|||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</p>
|
</p>
|
||||||
|
{expanded && checkpoints.length > 0 ? (
|
||||||
|
<ol
|
||||||
|
data-testid="autopilot-steps"
|
||||||
|
className="flex w-full max-w-md flex-col gap-1 text-left text-ui-body-sm"
|
||||||
|
>
|
||||||
|
{checkpoints.map((checkpoint, index) => (
|
||||||
|
<li key={`${checkpoint.at}-${index}`} className="flex gap-2">
|
||||||
|
<span className="w-16 shrink-0 text-right tabular-nums">
|
||||||
|
{time.format(new Date(checkpoint.at))}
|
||||||
|
</span>
|
||||||
|
<span dir="auto" className="min-w-0 text-foreground/80">
|
||||||
|
{checkpoint.text}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
) : null}
|
||||||
{block.event === "autopilot.finished" && block.text ? (
|
{block.event === "autopilot.finished" && block.text ? (
|
||||||
<p dir="auto" className="max-w-xl text-ui-body-sm text-foreground/80">
|
<p
|
||||||
|
dir="auto"
|
||||||
|
data-testid="autopilot-result"
|
||||||
|
className="mt-1 w-full max-w-xl whitespace-pre-wrap rounded-lg border border-border bg-card px-4 py-3 text-left text-ui-body-sm text-card-foreground"
|
||||||
|
>
|
||||||
{block.text}
|
{block.text}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@ -11,13 +11,21 @@ import {
|
|||||||
serializeComposerPrompt,
|
serializeComposerPrompt,
|
||||||
truncateSlashDescription,
|
truncateSlashDescription,
|
||||||
} from "@rakazo/core";
|
} from "@rakazo/core";
|
||||||
import { BotAvatar, Button } from "@rakazo/ui-web";
|
import {
|
||||||
|
BotAvatar,
|
||||||
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@rakazo/ui-web";
|
||||||
import {
|
import {
|
||||||
ArrowUp,
|
ArrowUp,
|
||||||
Box,
|
Box,
|
||||||
Clock,
|
Clock,
|
||||||
Mic,
|
Mic,
|
||||||
Paperclip,
|
Paperclip,
|
||||||
|
Play,
|
||||||
Plus,
|
Plus,
|
||||||
Puzzle,
|
Puzzle,
|
||||||
Settings,
|
Settings,
|
||||||
@ -28,12 +36,16 @@ import type { ClipboardEvent, DragEvent, RefObject } from "react";
|
|||||||
import { memo, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
import { memo, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||||
import { VoiceAuroraRim } from "../../components/voice/VoiceAurora";
|
import { VoiceAuroraRim } from "../../components/voice/VoiceAurora";
|
||||||
import { isFileDrag, isFilePaste } from "../../lib/pending-attachments";
|
import { isFileDrag, isFilePaste } from "../../lib/pending-attachments";
|
||||||
|
import { AutopilotDialog } from "./autopilot-dialog";
|
||||||
import { previewMessageText } from "./message-preview";
|
import { previewMessageText } from "./message-preview";
|
||||||
import type { PendingAttachment } from "./shell-types";
|
import type { PendingAttachment } from "./shell-types";
|
||||||
import { FALLBACK_BOT_COLOR } from "./shell-types";
|
import { FALLBACK_BOT_COLOR } from "./shell-types";
|
||||||
|
|
||||||
const ATTACHMENT_ACCEPT = ATTACHMENT_ALLOWED_MIME_TYPES.join(",");
|
const ATTACHMENT_ACCEPT = ATTACHMENT_ALLOWED_MIME_TYPES.join(",");
|
||||||
|
|
||||||
|
const addButtonClass =
|
||||||
|
"tactile-press size-10 shrink-0 rounded-full border border-border bg-foreground/5 text-foreground/80 transition-all hover:bg-foreground/10 hover:text-foreground flex items-center justify-center cursor-pointer";
|
||||||
|
|
||||||
export const Composer = memo(function Composer({
|
export const Composer = memo(function Composer({
|
||||||
running,
|
running,
|
||||||
disabled,
|
disabled,
|
||||||
@ -62,6 +74,7 @@ export const Composer = memo(function Composer({
|
|||||||
onSlashOpen,
|
onSlashOpen,
|
||||||
onSlashAction,
|
onSlashAction,
|
||||||
onConnectModel,
|
onConnectModel,
|
||||||
|
onAutopilot,
|
||||||
}: {
|
}: {
|
||||||
running: boolean;
|
running: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@ -91,9 +104,12 @@ export const Composer = memo(function Composer({
|
|||||||
onSlashAction?: (action: SlashActionId) => void;
|
onSlashAction?: (action: SlashActionId) => void;
|
||||||
/** Set while no model is connected: nothing can run until one is. */
|
/** Set while no model is connected: nothing can run until one is. */
|
||||||
onConnectModel?: () => void;
|
onConnectModel?: () => void;
|
||||||
|
/** Hand the order to the autopilot; the composer menu offers it only when set. */
|
||||||
|
onAutopilot?: (goal: string, until?: string) => Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const [draft, setDraft] = useState("");
|
const [draft, setDraft] = useState("");
|
||||||
|
const [autopilotOpen, setAutopilotOpen] = useState(false);
|
||||||
const [mentionQuery, setMentionQuery] = useState<string | null>(null);
|
const [mentionQuery, setMentionQuery] = useState<string | null>(null);
|
||||||
const [mentionHighlightIndex, setMentionHighlightIndex] = useState(0);
|
const [mentionHighlightIndex, setMentionHighlightIndex] = useState(0);
|
||||||
const [slashQuery, setSlashQuery] = useState<string | null>(null);
|
const [slashQuery, setSlashQuery] = useState<string | null>(null);
|
||||||
@ -571,16 +587,54 @@ export const Composer = memo(function Composer({
|
|||||||
className="hidden"
|
className="hidden"
|
||||||
onChange={(event) => void onAttachmentPick(event.target.files)}
|
onChange={(event) => void onAttachmentPick(event.target.files)}
|
||||||
/>
|
/>
|
||||||
<Button
|
{onAutopilot ? (
|
||||||
variant="ghost"
|
<DropdownMenu>
|
||||||
size="icon"
|
<DropdownMenuTrigger
|
||||||
aria-label={t`Attach file`}
|
disabled={disabled}
|
||||||
disabled={disabled}
|
render={
|
||||||
onClick={() => fileInputRef.current?.click()}
|
<Button
|
||||||
className="tactile-press size-10 shrink-0 rounded-full border border-border bg-foreground/5 text-foreground/80 transition-all hover:bg-foreground/10 hover:text-foreground flex items-center justify-center cursor-pointer"
|
variant="ghost"
|
||||||
>
|
size="icon"
|
||||||
<Plus strokeWidth={2.2} className="size-icon-lg" />
|
aria-label={t`Add`}
|
||||||
</Button>
|
className={addButtonClass}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Plus strokeWidth={2.2} className="size-icon-lg" />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent side="top" className="w-auto min-w-40">
|
||||||
|
<DropdownMenuItem onClick={() => fileInputRef.current?.click()}>
|
||||||
|
<Paperclip />
|
||||||
|
{t`Attach file`}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setAutopilotOpen(true)}>
|
||||||
|
<Play />
|
||||||
|
{t`Autopilot`}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
aria-label={t`Attach file`}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
className={addButtonClass}
|
||||||
|
>
|
||||||
|
<Plus strokeWidth={2.2} className="size-icon-lg" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{autopilotOpen && onAutopilot ? (
|
||||||
|
<AutopilotDialog
|
||||||
|
draft={draft}
|
||||||
|
onClose={() => setAutopilotOpen(false)}
|
||||||
|
onStart={async (goal, until) => {
|
||||||
|
await onAutopilot(goal, until);
|
||||||
|
setDraft("");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<div className="flex min-w-0 flex-1 flex-wrap items-end gap-1.5">
|
<div className="flex min-w-0 flex-1 flex-wrap items-end gap-1.5">
|
||||||
{selectedSkill ? (
|
{selectedSkill ? (
|
||||||
<span
|
<span
|
||||||
|
|||||||
@ -100,7 +100,7 @@ describePostgres("autopilot (PostgreSQL)", () => {
|
|||||||
expect(rules.map((rule) => rule.matchValue)).toEqual(["github_commit"]);
|
expect(rules.map((rule) => rule.matchValue)).toEqual(["github_commit"]);
|
||||||
|
|
||||||
for (let step = 1; step <= 4; step += 1) {
|
for (let step = 1; step <= 4; step += 1) {
|
||||||
await checkpointAutopilot(prisma, owner, { summary: `Ticket ${step} tidied` });
|
await checkpointAutopilot(deps, owner, { summary: `Ticket ${step} tidied` });
|
||||||
await continueAutopilotAfterRun(deps, owner, { checkpointed: true, failed: false });
|
await continueAutopilotAfterRun(deps, owner, { checkpointed: true, failed: false });
|
||||||
await finishQueuedRuns(owner.threadId);
|
await finishQueuedRuns(owner.threadId);
|
||||||
}
|
}
|
||||||
@ -129,6 +129,11 @@ describePostgres("autopilot (PostgreSQL)", () => {
|
|||||||
["autopilot.started", true],
|
["autopilot.started", true],
|
||||||
["autopilot.finished", false],
|
["autopilot.finished", false],
|
||||||
]);
|
]);
|
||||||
|
// The start line carries the live progress.
|
||||||
|
expect(timeline[0]?.steps).toBe(4);
|
||||||
|
expect(timeline[0]?.checkpoints?.map((checkpoint) => checkpoint.text)).toEqual(
|
||||||
|
[1, 2, 3, 4].map((step) => `Ticket ${step} tidied`),
|
||||||
|
);
|
||||||
// Nothing runs after the end.
|
// Nothing runs after the end.
|
||||||
await continueAutopilotAfterRun(deps, owner, { checkpointed: false, failed: false });
|
await continueAutopilotAfterRun(deps, owner, { checkpointed: false, failed: false });
|
||||||
expect(enqueue).toHaveBeenCalledTimes(4);
|
expect(enqueue).toHaveBeenCalledTimes(4);
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
appendEventInTransaction,
|
appendEventInTransaction,
|
||||||
endAutopilotLinesInTransaction,
|
endAutopilotLinesInTransaction,
|
||||||
postAutopilotLineInTransaction,
|
postAutopilotLineInTransaction,
|
||||||
|
recordAutopilotCheckpointInTransaction,
|
||||||
} from "@rakazo/db";
|
} from "@rakazo/db";
|
||||||
import { getLogger } from "@rakazo/logging";
|
import { getLogger } from "@rakazo/logging";
|
||||||
|
|
||||||
@ -165,22 +166,40 @@ export async function startAutopilot(
|
|||||||
|
|
||||||
/** A step is done: remember where the work stands. */
|
/** A step is done: remember where the work stands. */
|
||||||
export async function checkpointAutopilot(
|
export async function checkpointAutopilot(
|
||||||
prisma: PrismaClient,
|
deps: Pick<AutopilotDeps, "prisma" | "events">,
|
||||||
owner: Pick<AutopilotOwner, "userId" | "threadId">,
|
owner: Pick<AutopilotOwner, "userId" | "threadId">,
|
||||||
input: { summary: string; next?: string },
|
input: { summary: string; next?: string },
|
||||||
): Promise<{ ok: true; steps: number } | { error: string }> {
|
): Promise<{ ok: true; steps: number } | { error: string }> {
|
||||||
const open = await findOpenAutopilot(prisma, owner);
|
const open = await findOpenAutopilot(deps.prisma, owner);
|
||||||
if (open?.status !== "active") return { error: "No autopilot is running in this thread." };
|
if (open?.status !== "active") return { error: "No autopilot is running in this thread." };
|
||||||
const updated = await prisma.autopilot.update({
|
const summary = clip(input.summary, AUTOPILOT_LIMITS.checkpoint);
|
||||||
where: { id: open.id },
|
const recorded = await deps.prisma.$transaction(async (tx) => {
|
||||||
data: {
|
const autopilot = await lockAutopilot(tx, open.id);
|
||||||
stepCount: { increment: 1 },
|
if (autopilot?.status !== "active") return null;
|
||||||
idleCount: 0,
|
const updated = await tx.autopilot.update({
|
||||||
lastCheckpoint: clip(input.summary, AUTOPILOT_LIMITS.checkpoint),
|
where: { id: autopilot.id },
|
||||||
nextStep: input.next?.trim() ? clip(input.next, AUTOPILOT_LIMITS.checkpoint) : null,
|
data: {
|
||||||
},
|
stepCount: { increment: 1 },
|
||||||
|
idleCount: 0,
|
||||||
|
lastCheckpoint: summary,
|
||||||
|
nextStep: input.next?.trim() ? clip(input.next, AUTOPILOT_LIMITS.checkpoint) : null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const seq = autopilot.startMessageId
|
||||||
|
? await recordAutopilotCheckpointInTransaction(tx, {
|
||||||
|
spaceId: autopilot.spaceId,
|
||||||
|
threadId: autopilot.threadId,
|
||||||
|
messageId: autopilot.startMessageId,
|
||||||
|
autopilotId: autopilot.id,
|
||||||
|
checkpoint: { text: summary, at: new Date().toISOString() },
|
||||||
|
steps: updated.stepCount,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
return { steps: updated.stepCount, threadId: autopilot.threadId, seq };
|
||||||
});
|
});
|
||||||
return { ok: true, steps: updated.stepCount };
|
if (!recorded) return { error: "No autopilot is running in this thread." };
|
||||||
|
await notify(deps.events, recorded.threadId, recorded.seq);
|
||||||
|
return { ok: true, steps: recorded.steps };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** End the autopilot: finished with a result, or stopped by the user. */
|
/** End the autopilot: finished with a result, or stopped by the user. */
|
||||||
@ -366,7 +385,7 @@ export async function continueAutopilotAfterRun(
|
|||||||
deps: AutopilotDeps,
|
deps: AutopilotDeps,
|
||||||
run: { userId: string; threadId: string; botId: string },
|
run: { userId: string; threadId: string; botId: string },
|
||||||
outcome: { checkpointed: boolean; failed: boolean; error?: string },
|
outcome: { checkpointed: boolean; failed: boolean; error?: string },
|
||||||
): Promise<void> {
|
): Promise<{ paused: string } | null> {
|
||||||
const open = await deps.prisma.autopilot.findFirst({
|
const open = await deps.prisma.autopilot.findFirst({
|
||||||
where: {
|
where: {
|
||||||
threadId: run.threadId,
|
threadId: run.threadId,
|
||||||
@ -376,7 +395,7 @@ export async function continueAutopilotAfterRun(
|
|||||||
},
|
},
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
});
|
});
|
||||||
if (!open) return;
|
if (!open) return null;
|
||||||
const next = await deps.prisma.$transaction(async (tx) => {
|
const next = await deps.prisma.$transaction(async (tx) => {
|
||||||
const autopilot = await lockAutopilot(tx, open.id);
|
const autopilot = await lockAutopilot(tx, open.id);
|
||||||
if (autopilot?.status !== "active") return null;
|
if (autopilot?.status !== "active") return null;
|
||||||
@ -390,7 +409,7 @@ export async function continueAutopilotAfterRun(
|
|||||||
? `Runs keep failing${outcome.error ? `: ${clip(outcome.error, 200)}` : "."}`
|
? `Runs keep failing${outcome.error ? `: ${clip(outcome.error, 200)}` : "."}`
|
||||||
: `No progress in ${guard.idleCount} runs${autopilot.lastCheckpoint ? `. Last: ${autopilot.lastCheckpoint}` : "."}`;
|
: `No progress in ${guard.idleCount} runs${autopilot.lastCheckpoint ? `. Last: ${autopilot.lastCheckpoint}` : "."}`;
|
||||||
const line = await pauseAutopilotInTransaction(tx, autopilot, reason);
|
const line = await pauseAutopilotInTransaction(tx, autopilot, reason);
|
||||||
return { threadId: autopilot.threadId, seq: line.seq, runId: null };
|
return { threadId: autopilot.threadId, seq: line.seq, runId: null, paused: reason };
|
||||||
}
|
}
|
||||||
const updated = await tx.autopilot.update({
|
const updated = await tx.autopilot.update({
|
||||||
where: { id: autopilot.id },
|
where: { id: autopilot.id },
|
||||||
@ -400,17 +419,19 @@ export async function continueAutopilotAfterRun(
|
|||||||
threadId: autopilot.threadId,
|
threadId: autopilot.threadId,
|
||||||
seq: null,
|
seq: null,
|
||||||
runId: await createContinuationRun(tx, updated),
|
runId: await createContinuationRun(tx, updated),
|
||||||
|
paused: null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
if (!next) return;
|
if (!next) return null;
|
||||||
if ("expired" in next && next.expired) {
|
if ("expired" in next && next.expired) {
|
||||||
const autopilot = next.expired;
|
const autopilot = next.expired;
|
||||||
await endAutopilot(deps, autopilot.id, {
|
await endAutopilot(deps, autopilot.id, {
|
||||||
status: "finished",
|
status: "finished",
|
||||||
result: autopilot.lastCheckpoint ?? "Time is up.",
|
result: autopilot.lastCheckpoint ?? "Time is up.",
|
||||||
});
|
});
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
await notify(deps.events, next.threadId, next.seq);
|
await notify(deps.events, next.threadId, next.seq);
|
||||||
await enqueue(deps, next.runId);
|
await enqueue(deps, next.runId);
|
||||||
|
return next.paused ? { paused: next.paused } : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -467,6 +467,31 @@ function isFailedToolResult(value: unknown): value is { error: unknown } {
|
|||||||
return error !== undefined && error !== null;
|
return error !== undefined && error !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hand a finished run to its thread's autopilot. A pause is the one autopilot event that pushes
|
||||||
|
* a notification: the user decides whether it goes on.
|
||||||
|
*/
|
||||||
|
async function continueAutopilot(
|
||||||
|
deps: ExecutorDeps,
|
||||||
|
run: { spaceId: string; userId: string; botId: string; threadId: string },
|
||||||
|
bot: { id: string; name: string },
|
||||||
|
threadId: string,
|
||||||
|
outcome: { checkpointed: boolean; failed: boolean; error?: string },
|
||||||
|
) {
|
||||||
|
const result = await continueAutopilotAfterRun(deps, run, outcome).catch((error) => {
|
||||||
|
getLogger().error("autopilot continuation", error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
if (!result) return;
|
||||||
|
await notifyRun(deps, run, {
|
||||||
|
kind: "help",
|
||||||
|
title: `${bot.name} paused the autopilot`,
|
||||||
|
body: result.paused.slice(0, 180),
|
||||||
|
botId: bot.id,
|
||||||
|
threadId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function toolCompletionFromResult(
|
export function toolCompletionFromResult(
|
||||||
base: Pick<AgentToolCompletion, "name" | "executionId" | "durationMs">,
|
base: Pick<AgentToolCompletion, "name" | "executionId" | "durationMs">,
|
||||||
result: unknown,
|
result: unknown,
|
||||||
@ -1373,11 +1398,11 @@ export function createRunExecutor(deps: ExecutorDeps) {
|
|||||||
.enqueue(runContinueJob(failed.continuationRunId))
|
.enqueue(runContinueJob(failed.continuationRunId))
|
||||||
.catch((error) => getLogger().error("steering continuation enqueue", error));
|
.catch((error) => getLogger().error("steering continuation enqueue", error));
|
||||||
} else {
|
} else {
|
||||||
await continueAutopilotAfterRun(deps, run, {
|
await continueAutopilot(deps, run, bot, thread.id, {
|
||||||
checkpointed: false,
|
checkpointed: false,
|
||||||
failed: true,
|
failed: true,
|
||||||
error: MISSING_MODEL_MESSAGE,
|
error: MISSING_MODEL_MESSAGE,
|
||||||
}).catch((continueError) => getLogger().error("autopilot continuation", continueError));
|
});
|
||||||
}
|
}
|
||||||
if (run.trigger === "bot_message") {
|
if (run.trigger === "bot_message") {
|
||||||
await returnBotMessageOutcome(
|
await returnBotMessageOutcome(
|
||||||
@ -1388,7 +1413,8 @@ export function createRunExecutor(deps: ExecutorDeps) {
|
|||||||
"status",
|
"status",
|
||||||
).catch((error) => getLogger().error("bot message failure return", error));
|
).catch((error) => getLogger().error("bot message failure return", error));
|
||||||
}
|
}
|
||||||
if (!failed.continuationRunId) {
|
// An autopilot keeps the night quiet: only a pause, a real blocker, reaches the user.
|
||||||
|
if (!failed.continuationRunId && run.trigger !== "autopilot") {
|
||||||
await notifyRun(deps, run, {
|
await notifyRun(deps, run, {
|
||||||
kind: "failure",
|
kind: "failure",
|
||||||
title: `${bot.name} failed`,
|
title: `${bot.name} failed`,
|
||||||
@ -3594,7 +3620,7 @@ export function createRunExecutor(deps: ExecutorDeps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (name === "autopilot_checkpoint") {
|
if (name === "autopilot_checkpoint") {
|
||||||
const checkpoint = await checkpointAutopilot(deps.prisma, owner, {
|
const checkpoint = await checkpointAutopilot(deps, owner, {
|
||||||
summary: text("summary"),
|
summary: text("summary"),
|
||||||
next: text("next") || undefined,
|
next: text("next") || undefined,
|
||||||
});
|
});
|
||||||
@ -4656,10 +4682,10 @@ export function createRunExecutor(deps: ExecutorDeps) {
|
|||||||
.enqueue(runContinueJob(completed.continuationRunId))
|
.enqueue(runContinueJob(completed.continuationRunId))
|
||||||
.catch((error) => getLogger().error("steering continuation enqueue", error));
|
.catch((error) => getLogger().error("steering continuation enqueue", error));
|
||||||
} else {
|
} else {
|
||||||
await continueAutopilotAfterRun(deps, run, {
|
await continueAutopilot(deps, run, bot, thread.id, {
|
||||||
checkpointed: autopilotCheckpointed,
|
checkpointed: autopilotCheckpointed,
|
||||||
failed: false,
|
failed: false,
|
||||||
}).catch((error) => getLogger().error("autopilot continuation", error));
|
});
|
||||||
}
|
}
|
||||||
if (botMessageOutcome) {
|
if (botMessageOutcome) {
|
||||||
// Prefer the final reply. If the turn only posted mid-turn progress, return that
|
// Prefer the final reply. If the turn only posted mid-turn progress, return that
|
||||||
@ -4674,7 +4700,7 @@ export function createRunExecutor(deps: ExecutorDeps) {
|
|||||||
botMessageOutcome.intent,
|
botMessageOutcome.intent,
|
||||||
).catch((error) => getLogger().error("bot message result return", error));
|
).catch((error) => getLogger().error("bot message result return", error));
|
||||||
}
|
}
|
||||||
if (text && !completed.continuationRunId) {
|
if (text && !completed.continuationRunId && run.trigger !== "autopilot") {
|
||||||
await notifyRun(deps, run, {
|
await notifyRun(deps, run, {
|
||||||
kind: "completion",
|
kind: "completion",
|
||||||
title: `${bot.name} finished`,
|
title: `${bot.name} finished`,
|
||||||
@ -4733,11 +4759,11 @@ export function createRunExecutor(deps: ExecutorDeps) {
|
|||||||
.enqueue(runContinueJob(failed.continuationRunId))
|
.enqueue(runContinueJob(failed.continuationRunId))
|
||||||
.catch((error) => getLogger().error("steering continuation enqueue", error));
|
.catch((error) => getLogger().error("steering continuation enqueue", error));
|
||||||
} else {
|
} else {
|
||||||
await continueAutopilotAfterRun(deps, run, {
|
await continueAutopilot(deps, run, bot, thread.id, {
|
||||||
checkpointed: autopilotCheckpointed,
|
checkpointed: autopilotCheckpointed,
|
||||||
failed: true,
|
failed: true,
|
||||||
error: message,
|
error: message,
|
||||||
}).catch((continueError) => getLogger().error("autopilot continuation", continueError));
|
});
|
||||||
}
|
}
|
||||||
if (run.trigger === "bot_message") {
|
if (run.trigger === "bot_message") {
|
||||||
await returnBotMessageOutcome(
|
await returnBotMessageOutcome(
|
||||||
@ -4748,7 +4774,8 @@ export function createRunExecutor(deps: ExecutorDeps) {
|
|||||||
"status",
|
"status",
|
||||||
).catch((returnError) => getLogger().error("bot message failure return", returnError));
|
).catch((returnError) => getLogger().error("bot message failure return", returnError));
|
||||||
}
|
}
|
||||||
if (!failed.continuationRunId) {
|
// An autopilot keeps the night quiet: only a pause, a real blocker, reaches the user.
|
||||||
|
if (!failed.continuationRunId && run.trigger !== "autopilot") {
|
||||||
await notifyRun(deps, run, {
|
await notifyRun(deps, run, {
|
||||||
kind: "failure",
|
kind: "failure",
|
||||||
title: `${bot.name} failed`,
|
title: `${bot.name} failed`,
|
||||||
|
|||||||
@ -6,6 +6,8 @@ export const AUTOPILOT_IDLE_LIMIT = 3;
|
|||||||
export const AUTOPILOT_FAIL_LIMIT = 3;
|
export const AUTOPILOT_FAIL_LIMIT = 3;
|
||||||
|
|
||||||
export const AUTOPILOT_LIMITS = { goal: 500, focus: 200, checkpoint: 300, result: 2000 } as const;
|
export const AUTOPILOT_LIMITS = { goal: 500, focus: 200, checkpoint: 300, result: 2000 } as const;
|
||||||
|
/** Checkpoints kept on the start line; older ones only count toward the step total. */
|
||||||
|
export const AUTOPILOT_CHECKPOINT_LOG = 50;
|
||||||
|
|
||||||
export const AutopilotStatusSchema = z.enum(["active", "paused", "finished", "stopped"]);
|
export const AutopilotStatusSchema = z.enum(["active", "paused", "finished", "stopped"]);
|
||||||
export type AutopilotStatus = z.infer<typeof AutopilotStatusSchema>;
|
export type AutopilotStatus = z.infer<typeof AutopilotStatusSchema>;
|
||||||
@ -30,6 +32,11 @@ export const AutopilotBlockSchema = z.object({
|
|||||||
/** Tools or apps the bot may use without asking while this autopilot runs. */
|
/** Tools or apps the bot may use without asking while this autopilot runs. */
|
||||||
allowed: z.array(z.string()).optional(),
|
allowed: z.array(z.string()).optional(),
|
||||||
steps: z.number().int().nonnegative().optional(),
|
steps: z.number().int().nonnegative().optional(),
|
||||||
|
/** The latest checkpoints, newest last, on the start line. */
|
||||||
|
checkpoints: z
|
||||||
|
.array(z.object({ text: z.string(), at: z.string() }))
|
||||||
|
.max(AUTOPILOT_CHECKPOINT_LOG)
|
||||||
|
.optional(),
|
||||||
durationMs: z.number().int().nonnegative().optional(),
|
durationMs: z.number().int().nonnegative().optional(),
|
||||||
/** Why it paused, or what it achieved. */
|
/** Why it paused, or what it achieved. */
|
||||||
text: z.string().optional(),
|
text: z.string().optional(),
|
||||||
@ -55,6 +62,16 @@ export const AutopilotSchema = z.object({
|
|||||||
});
|
});
|
||||||
export type Autopilot = z.infer<typeof AutopilotSchema>;
|
export type Autopilot = z.infer<typeof AutopilotSchema>;
|
||||||
|
|
||||||
|
/** How long a finished or stopped autopilot stays in Today, so the night's work is there at breakfast. */
|
||||||
|
export const AUTOPILOT_RECENT_MS = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
/** An autopilot in Today: open ones, and those that ended within AUTOPILOT_RECENT_MS. */
|
||||||
|
export const AutopilotRecentSchema = AutopilotSchema.extend({
|
||||||
|
spaceId: Id,
|
||||||
|
groupId: Id.nullable(),
|
||||||
|
});
|
||||||
|
export type AutopilotRecent = z.infer<typeof AutopilotRecentSchema>;
|
||||||
|
|
||||||
export const AutopilotStartInputSchema = z.object({
|
export const AutopilotStartInputSchema = z.object({
|
||||||
goal: z.string().trim().min(1).max(AUTOPILOT_LIMITS.goal),
|
goal: z.string().trim().min(1).max(AUTOPILOT_LIMITS.goal),
|
||||||
focus: z.string().trim().max(AUTOPILOT_LIMITS.focus).optional(),
|
focus: z.string().trim().max(AUTOPILOT_LIMITS.focus).optional(),
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { eventIterator, oc } from "@orpc/contract";
|
|||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
import { AiConsentQuerySchema, AiConsentStatusSchema } from "./ai-consent.js";
|
import { AiConsentQuerySchema, AiConsentStatusSchema } from "./ai-consent.js";
|
||||||
import { ATTACHMENT_MAX_BASE64_LENGTH, ATTACHMENT_MAX_COUNT } from "./attachments.js";
|
import { ATTACHMENT_MAX_BASE64_LENGTH, ATTACHMENT_MAX_COUNT } from "./attachments.js";
|
||||||
import { AutopilotSchema, AutopilotStartInputSchema } from "./autopilot.js";
|
import { AutopilotRecentSchema, AutopilotSchema, AutopilotStartInputSchema } from "./autopilot.js";
|
||||||
import { CardActSchema } from "./cards.js";
|
import { CardActSchema } from "./cards.js";
|
||||||
import {
|
import {
|
||||||
CheckInSchema,
|
CheckInSchema,
|
||||||
@ -948,6 +948,8 @@ export const appContract = {
|
|||||||
.output(AutopilotSchema),
|
.output(AutopilotSchema),
|
||||||
stop: oc.input(threadTarget).output(z.object({ ok: z.literal(true) })),
|
stop: oc.input(threadTarget).output(z.object({ ok: z.literal(true) })),
|
||||||
resume: oc.input(threadTarget).output(z.object({ ok: z.literal(true) })),
|
resume: oc.input(threadTarget).output(z.object({ ok: z.literal(true) })),
|
||||||
|
/** The user's autopilots in this space that run, wait, or ended within the last day. */
|
||||||
|
recent: oc.output(z.object({ items: z.array(AutopilotRecentSchema) })),
|
||||||
},
|
},
|
||||||
voice: {
|
voice: {
|
||||||
catalog: oc.output(z.array(VoiceCatalogEntrySchema)),
|
catalog: oc.output(z.array(VoiceCatalogEntrySchema)),
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import type { AutopilotBlock } from "@rakazo/contracts";
|
import type { AutopilotBlock } from "@rakazo/contracts";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
|
addAutopilotCheckpoint,
|
||||||
advanceAutopilotGuard,
|
advanceAutopilotGuard,
|
||||||
autopilotContinuationPrompt,
|
autopilotContinuationPrompt,
|
||||||
autopilotExpired,
|
autopilotExpired,
|
||||||
autopilotHistoryText,
|
autopilotHistoryText,
|
||||||
|
autopilotUntil,
|
||||||
formatAutopilotDuration,
|
formatAutopilotDuration,
|
||||||
markAutopilotLinesEnded,
|
markAutopilotLinesEnded,
|
||||||
} from "./autopilot.js";
|
} from "./autopilot.js";
|
||||||
@ -82,4 +84,31 @@ describe("autopilot", () => {
|
|||||||
expect(formatAutopilotDuration(222 * 60_000)).toBe("3 h 42 min");
|
expect(formatAutopilotDuration(222 * 60_000)).toBe("3 h 42 min");
|
||||||
expect(formatAutopilotDuration(120 * 60_000)).toBe("2 h");
|
expect(formatAutopilotDuration(120 * 60_000)).toBe("2 h");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("counts checkpoints on the start line and keeps the latest fifty", () => {
|
||||||
|
let blocks: AutopilotBlock[] = [started];
|
||||||
|
for (let step = 1; step <= 52; step += 1) {
|
||||||
|
blocks =
|
||||||
|
addAutopilotCheckpoint(blocks, "a1", { text: `Step ${step}`, at: "t" }, step) ?? blocks;
|
||||||
|
}
|
||||||
|
expect(blocks[0]?.steps).toBe(52);
|
||||||
|
expect(blocks[0]?.checkpoints).toHaveLength(50);
|
||||||
|
expect(blocks[0]?.checkpoints?.at(-1)?.text).toBe("Step 52");
|
||||||
|
expect(addAutopilotCheckpoint(blocks, "other", { text: "x", at: "t" }, 1)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("picks the next morning or the next given time", () => {
|
||||||
|
const evening = new Date(2026, 9, 4, 22, 30);
|
||||||
|
expect(autopilotUntil({ kind: "done" }, evening)).toBeUndefined();
|
||||||
|
expect(autopilotUntil({ kind: "morning" }, evening)).toEqual(new Date(2026, 9, 5, 8, 0));
|
||||||
|
expect(autopilotUntil({ kind: "morning" }, new Date(2026, 9, 5, 2, 0))).toEqual(
|
||||||
|
new Date(2026, 9, 5, 8, 0),
|
||||||
|
);
|
||||||
|
expect(autopilotUntil({ kind: "time", time: "23:15" }, evening)).toEqual(
|
||||||
|
new Date(2026, 9, 4, 23, 15),
|
||||||
|
);
|
||||||
|
expect(autopilotUntil({ kind: "time", time: "06:00" }, evening)).toEqual(
|
||||||
|
new Date(2026, 9, 5, 6, 0),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
import type { AutopilotBlock } from "@rakazo/contracts";
|
import type { AutopilotBlock } from "@rakazo/contracts";
|
||||||
import { AUTOPILOT_FAIL_LIMIT, AUTOPILOT_IDLE_LIMIT } from "@rakazo/contracts";
|
import {
|
||||||
|
AUTOPILOT_CHECKPOINT_LOG,
|
||||||
|
AUTOPILOT_FAIL_LIMIT,
|
||||||
|
AUTOPILOT_IDLE_LIMIT,
|
||||||
|
} from "@rakazo/contracts";
|
||||||
|
|
||||||
/** What the autopilot remembers between runs. */
|
/** What the autopilot remembers between runs. */
|
||||||
export interface AutopilotState {
|
export interface AutopilotState {
|
||||||
@ -86,6 +90,45 @@ export function markAutopilotLinesEnded(
|
|||||||
return changed ? next : null;
|
return changed ? next : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Count a checkpoint on the autopilot's start line and keep the latest few. */
|
||||||
|
export function addAutopilotCheckpoint(
|
||||||
|
blocks: readonly { kind: string }[],
|
||||||
|
autopilotId: string,
|
||||||
|
checkpoint: { text: string; at: string },
|
||||||
|
steps: number,
|
||||||
|
): AutopilotBlock[] | null {
|
||||||
|
let changed = false;
|
||||||
|
const next = blocks.map((block) => {
|
||||||
|
const line = block as AutopilotBlock;
|
||||||
|
if (line.kind !== "autopilot" || line.autopilotId !== autopilotId) return line;
|
||||||
|
if (line.event !== "autopilot.started") return line;
|
||||||
|
changed = true;
|
||||||
|
return {
|
||||||
|
...line,
|
||||||
|
steps,
|
||||||
|
checkpoints: [...(line.checkpoints ?? []), checkpoint].slice(-AUTOPILOT_CHECKPOINT_LOG),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return changed ? next : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** When the autopilot should stop: never on its own, at the next 8:00, or at the next given time. */
|
||||||
|
export type AutopilotUntilChoice =
|
||||||
|
| { kind: "done" }
|
||||||
|
| { kind: "morning" }
|
||||||
|
| { kind: "time"; time: string };
|
||||||
|
|
||||||
|
export function autopilotUntil(choice: AutopilotUntilChoice, now: Date): Date | undefined {
|
||||||
|
if (choice.kind === "done") return undefined;
|
||||||
|
const [hours, minutes] =
|
||||||
|
choice.kind === "morning" ? [8, 0] : choice.time.split(":").map((part) => Number(part));
|
||||||
|
if (!Number.isInteger(hours) || !Number.isInteger(minutes)) return undefined;
|
||||||
|
const until = new Date(now);
|
||||||
|
until.setHours(hours ?? 0, minutes ?? 0, 0, 0);
|
||||||
|
if (until.getTime() <= now.getTime()) until.setDate(until.getDate() + 1);
|
||||||
|
return until;
|
||||||
|
}
|
||||||
|
|
||||||
/** "3 h 42 min", "12 min", or "1 min" for anything shorter. */
|
/** "3 h 42 min", "12 min", or "1 min" for anything shorter. */
|
||||||
export function formatAutopilotDuration(ms: number): string {
|
export function formatAutopilotDuration(ms: number): string {
|
||||||
const minutes = Math.max(1, Math.round(ms / 60_000));
|
const minutes = Math.max(1, Math.round(ms / 60_000));
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { AutopilotBlock, MessageBlock } from "@rakazo/contracts";
|
import type { AutopilotBlock, MessageBlock } from "@rakazo/contracts";
|
||||||
import { markAutopilotLinesEnded } from "@rakazo/core";
|
import { addAutopilotCheckpoint, markAutopilotLinesEnded } from "@rakazo/core";
|
||||||
import type { Prisma } from "./client.js";
|
import type { Prisma } from "./client.js";
|
||||||
import { appendEventInTransaction } from "./events.js";
|
import { appendEventInTransaction } from "./events.js";
|
||||||
import { createThreadMessageInTransaction } from "./messages.js";
|
import { createThreadMessageInTransaction } from "./messages.js";
|
||||||
@ -57,3 +57,41 @@ export async function endAutopilotLinesInTransaction(
|
|||||||
}
|
}
|
||||||
return seq;
|
return seq;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Count a checkpoint on the autopilot's start line, so the line shows live progress. */
|
||||||
|
export async function recordAutopilotCheckpointInTransaction(
|
||||||
|
tx: Prisma.TransactionClient,
|
||||||
|
input: {
|
||||||
|
spaceId: string;
|
||||||
|
threadId: string;
|
||||||
|
messageId: string;
|
||||||
|
autopilotId: string;
|
||||||
|
checkpoint: { text: string; at: string };
|
||||||
|
steps: number;
|
||||||
|
},
|
||||||
|
): Promise<number | null> {
|
||||||
|
const message = await tx.message.findUnique({
|
||||||
|
where: { id: input.messageId },
|
||||||
|
select: { id: true, role: true, botId: true, blocks: true, threadId: true },
|
||||||
|
});
|
||||||
|
if (!message?.botId || message.threadId !== input.threadId) return null;
|
||||||
|
const blocks = addAutopilotCheckpoint(
|
||||||
|
message.blocks as MessageBlock[],
|
||||||
|
input.autopilotId,
|
||||||
|
input.checkpoint,
|
||||||
|
input.steps,
|
||||||
|
);
|
||||||
|
if (!blocks) return null;
|
||||||
|
await tx.message.update({
|
||||||
|
where: { id: message.id },
|
||||||
|
data: { blocks: blocks as Prisma.InputJsonValue },
|
||||||
|
});
|
||||||
|
const event = await appendEventInTransaction(tx, {
|
||||||
|
spaceId: input.spaceId,
|
||||||
|
threadId: input.threadId,
|
||||||
|
botId: message.botId,
|
||||||
|
type: "thread.message.updated",
|
||||||
|
payload: { messageId: message.id, role: message.role, blocks },
|
||||||
|
});
|
||||||
|
return event.seq;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user