evOo-local/apps/mobile/lib/routine-line.ts
cynarAI dbf37f86a8
feat: show routines the bot sets up from chat as one quiet line (#133)
* feat: show routines the bot sets up from chat as one quiet line

When someone mentions something recurring in passing ("every Sunday I plan the
week"), the bot now creates the routine right away and the conversation shows a
single line with the schedule, Edit and Undo, on web and mobile.

- New `event` message block for routine lines, kept in history so later turns
  know what was set up or removed.
- schedule_create reuses an active repeating routine of the same name instead
  of adding a duplicate, caps a bot at 50 active schedules, and falls back to
  default times when none was given.
- Removing a routine (Undo, the editor, or schedule_cancel) marks its lines
  removed via the new routines.noticeMessageId column.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmSCnoCgXqBp2fU3vwz4o1

* test: give the routine notice test a complete job publisher

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UmSCnoCgXqBp2fU3vwz4o1

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-04 20:36:53 +02:00

57 lines
2.0 KiB
TypeScript

import type { RoutineEventBlock } from "@rakazo/contracts";
import { routineScheduleShape } from "@rakazo/core";
import { dateLocaleForUi, t } from "./i18n";
/** 2024-01-07 is a Sunday, so day n of that week has weekday n. */
const weekdayName = (weekday: number, locale: string) =>
new Intl.DateTimeFormat(locale, { weekday: "long", timeZone: "UTC" }).format(
new Date(Date.UTC(2024, 0, 7 + weekday)),
);
const clock = (hour: number, minute: number, locale: string) =>
new Intl.DateTimeFormat(locale, { hour: "numeric", minute: "2-digit", timeZone: "UTC" }).format(
new Date(Date.UTC(2024, 0, 1, hour, minute)),
);
/** When a routine runs, in a few words of the active locale. */
export function routineLineSchedule(
block: Pick<RoutineEventBlock, "crons" | "nextRunAt" | "timezone">,
locale = dateLocaleForUi(),
): string {
const shape = routineScheduleShape(block.crons);
switch (shape.kind) {
case "hourly":
return t("Every hour");
case "minutes":
return t("Every {count} min", { count: shape.every });
case "hours":
return t("Every {count} h", { count: shape.every });
case "daily":
return t("Daily, {time}", { time: clock(shape.hour, shape.minute, locale) });
case "weekdays":
return t("Weekdays, {time}", { time: clock(shape.hour, shape.minute, locale) });
case "weekly":
return t("Every {day}, {time}", {
day: weekdayName(shape.weekday, locale),
time: clock(shape.hour, shape.minute, locale),
});
case "monthly":
return t("Monthly on day {day}, {time}", {
day: shape.day,
time: clock(shape.hour, shape.minute, locale),
});
default: {
if (!block.nextRunAt) return "";
const date = new Intl.DateTimeFormat(locale, {
weekday: "short",
day: "numeric",
month: "short",
hour: "numeric",
minute: "2-digit",
timeZone: block.timezone,
}).format(new Date(block.nextRunAt));
return shape.kind === "once" ? date : t("Next {date}", { date });
}
}
}