feat(trigger): add session-bound local triggers
This commit is contained in:
@@ -122,6 +122,9 @@ function formatAutomationSchedule(
|
||||
})
|
||||
: t("deleteConfirm.schedule.cron", { expr: job.schedule.expr });
|
||||
}
|
||||
if (job.schedule.kind === "external" || job.payload.kind === "external_trigger") {
|
||||
return t("deleteConfirm.schedule.external", { defaultValue: "External trigger" });
|
||||
}
|
||||
return t("deleteConfirm.schedule.unknown");
|
||||
}
|
||||
|
||||
@@ -131,6 +134,9 @@ function formatAutomationNextRun(
|
||||
locale: string,
|
||||
): string {
|
||||
if (!job.enabled) return t("deleteConfirm.next.disabled");
|
||||
if (job.schedule.kind === "external" || job.payload.kind === "external_trigger") {
|
||||
return t("deleteConfirm.next.external", { defaultValue: "Waiting for trigger" });
|
||||
}
|
||||
const next = job.state.next_run_at_ms;
|
||||
if (!next) return t("deleteConfirm.next.none");
|
||||
return t("deleteConfirm.next.label", { time: fmtDateTime(next, locale) });
|
||||
|
||||
@@ -167,7 +167,7 @@ export function MessageBubble({
|
||||
const reasoning = message.role === "assistant" ? message.reasoning ?? "" : "";
|
||||
const reasoningStreaming = !!(message.role === "assistant" && message.reasoningStreaming);
|
||||
const hasReasoning = reasoning.length > 0 || reasoningStreaming;
|
||||
const automationSourceLabel = message.source?.kind === "cron"
|
||||
const automationSourceLabel = message.source?.kind === "cron" || message.source?.kind === "trigger"
|
||||
? (message.source.label?.trim() || t("message.automationSourceFallback"))
|
||||
: "";
|
||||
const automationTriggeredLabel = t("message.automationTriggered");
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Cloud,
|
||||
Clipboard,
|
||||
Cpu,
|
||||
Database,
|
||||
Eye,
|
||||
@@ -106,6 +107,7 @@ import {
|
||||
updateWebSearchSettings,
|
||||
} from "@/lib/api";
|
||||
import { notifyCliAppsChanged } from "@/lib/cli-app-events";
|
||||
import { copyTextToClipboard } from "@/lib/clipboard";
|
||||
import { getHostApi } from "@/lib/runtime";
|
||||
import { notifyMcpPresetsChanged } from "@/lib/mcp-preset-events";
|
||||
import { fmtDateTime, relativeTime } from "@/lib/format";
|
||||
@@ -3671,6 +3673,7 @@ function AutomationListItem({
|
||||
const status = automationStatus(job, tx);
|
||||
const origin = automationOriginLabel(job, tx);
|
||||
const nextRun = formatAutomationNext(job, tx);
|
||||
const summary = automationSummary(job, tx);
|
||||
|
||||
return (
|
||||
<div role="listitem">
|
||||
@@ -3696,7 +3699,7 @@ function AutomationListItem({
|
||||
</span>
|
||||
</span>
|
||||
<span className="mt-1.5 line-clamp-2 text-[12px] leading-5 text-muted-foreground">
|
||||
{job.payload.message || tx("settings.automations.systemTask", "System-managed automation")}
|
||||
{summary}
|
||||
</span>
|
||||
<span className="mt-2.5 flex min-w-0 items-center gap-2 text-[11.5px] leading-none text-muted-foreground">
|
||||
<span className="truncate" title={formatAutomationNextTitle(job, locale, tx)}>
|
||||
@@ -3753,13 +3756,20 @@ function AutomationDetailPanel({
|
||||
: null;
|
||||
const created = job.created_at_ms ? fmtDateTime(job.created_at_ms, locale) : null;
|
||||
const updated = job.updated_at_ms ? fmtDateTime(job.updated_at_ms, locale) : null;
|
||||
const message = job.payload.message || tx("settings.automations.systemTask", "System-managed automation");
|
||||
const externalTrigger = isExternalTriggerAutomation(job);
|
||||
const triggerCommand = automationTriggerCommand(job);
|
||||
const message = automationDetailText(job, tx);
|
||||
const messageLabel = externalTrigger
|
||||
? tx("settings.automations.fields.command", "Command")
|
||||
: tx("settings.automations.fields.message", "Message");
|
||||
const schedule = formatAutomationSchedule(job, locale, tx);
|
||||
const [messageExpanded, setMessageExpanded] = useState(false);
|
||||
const [commandCopied, setCommandCopied] = useState(false);
|
||||
const messageNeedsExpansion = automationMessageNeedsExpansion(message);
|
||||
|
||||
useEffect(() => {
|
||||
setMessageExpanded(false);
|
||||
setCommandCopied(false);
|
||||
}, [job.id]);
|
||||
|
||||
return (
|
||||
@@ -3793,12 +3803,37 @@ function AutomationDetailPanel({
|
||||
<div className="grid min-h-0 min-w-0 flex-1 overflow-hidden lg:grid-cols-[minmax(0,1fr)_14.5rem]">
|
||||
<div className="min-h-0 min-w-0 space-y-3 overflow-y-auto overscroll-contain p-4 sm:p-5">
|
||||
<section className="rounded-[20px] border border-border/35 bg-background/62 px-4 py-3.5 shadow-[inset_0_1px_0_rgba(255,255,255,0.58)] dark:border-white/10 dark:bg-background/24">
|
||||
<div className="text-[11px] font-medium leading-none text-muted-foreground/75">
|
||||
{tx("settings.automations.fields.message", "Message")}
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-[11px] font-medium leading-none text-muted-foreground/75">
|
||||
{messageLabel}
|
||||
</div>
|
||||
{externalTrigger && triggerCommand ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 shrink-0 rounded-full px-2 text-[11.5px]"
|
||||
onClick={() => {
|
||||
void copyTextToClipboard(triggerCommand).then((ok) => {
|
||||
if (ok) setCommandCopied(true);
|
||||
});
|
||||
}}
|
||||
>
|
||||
{commandCopied ? (
|
||||
<Check className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
||||
) : (
|
||||
<Clipboard className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
||||
)}
|
||||
{commandCopied
|
||||
? tx("settings.automations.commandCopied", "Copied")
|
||||
: tx("settings.automations.copyCommand", "Copy")}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"mt-3 whitespace-pre-wrap break-words text-[13px] leading-6 text-foreground/85",
|
||||
externalTrigger && "font-mono text-[12.5px]",
|
||||
!messageExpanded && messageNeedsExpansion && "line-clamp-6",
|
||||
)}
|
||||
>
|
||||
@@ -3905,7 +3940,8 @@ function AutomationActionGroup({
|
||||
t(key, { defaultValue: fallback, ...(values ?? {}) });
|
||||
const canManage = !job.protected;
|
||||
const hasLinkedChat = Boolean(job.origin);
|
||||
const canRun = canManage && hasLinkedChat && job.enabled && !job.state.pending;
|
||||
const externalTrigger = isExternalTriggerAutomation(job);
|
||||
const canRun = canManage && hasLinkedChat && job.enabled && !job.state.pending && !externalTrigger;
|
||||
const toggleAction: AutomationAction = job.enabled ? "disable" : "enable";
|
||||
const canToggle = canManage && (job.enabled || hasLinkedChat);
|
||||
const toggleBusy = actionKey === `${toggleAction}:${job.id}`;
|
||||
@@ -3927,14 +3963,16 @@ function AutomationActionGroup({
|
||||
>
|
||||
<Pencil className="h-4 w-4" aria-hidden />
|
||||
</AppsActionButton>
|
||||
<AppsActionButton
|
||||
ariaLabel={tx("settings.automations.runNow", "Run now")}
|
||||
busy={actionKey === `run:${job.id}`}
|
||||
disabled={!canRun}
|
||||
onClick={() => void onAction("run", job)}
|
||||
>
|
||||
<PlayCircle className="h-4 w-4" aria-hidden />
|
||||
</AppsActionButton>
|
||||
{!externalTrigger ? (
|
||||
<AppsActionButton
|
||||
ariaLabel={tx("settings.automations.runNow", "Run now")}
|
||||
busy={actionKey === `run:${job.id}`}
|
||||
disabled={!canRun}
|
||||
onClick={() => void onAction("run", job)}
|
||||
>
|
||||
<PlayCircle className="h-4 w-4" aria-hidden />
|
||||
</AppsActionButton>
|
||||
) : null}
|
||||
<AppsActionButton
|
||||
ariaLabel={
|
||||
job.enabled
|
||||
@@ -4057,6 +4095,7 @@ function AutomationEditDialog({
|
||||
const tx = (key: string, fallback: string, values?: Record<string, unknown>) =>
|
||||
t(key, { defaultValue: fallback, ...(values ?? {}) });
|
||||
const [draft, setDraft] = useState<AutomationEditDraft>(() => automationDraftFromJob(null));
|
||||
const externalTrigger = isExternalTriggerAutomation(job);
|
||||
|
||||
useEffect(() => {
|
||||
setDraft(automationDraftFromJob(job));
|
||||
@@ -4106,34 +4145,38 @@ function AutomationEditDialog({
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block space-y-1.5">
|
||||
<span className="text-[12px] font-medium text-muted-foreground">
|
||||
{tx("settings.automations.fields.message", "Message")}
|
||||
</span>
|
||||
<Textarea
|
||||
value={draft.message}
|
||||
onChange={(event) => setDraft((prev) => ({ ...prev, message: event.target.value }))}
|
||||
className="min-h-[160px] resize-none rounded-[12px] text-[13px] leading-5"
|
||||
/>
|
||||
</label>
|
||||
{!externalTrigger ? (
|
||||
<label className="block space-y-1.5">
|
||||
<span className="text-[12px] font-medium text-muted-foreground">
|
||||
{tx("settings.automations.fields.message", "Message")}
|
||||
</span>
|
||||
<Textarea
|
||||
value={draft.message}
|
||||
onChange={(event) => setDraft((prev) => ({ ...prev, message: event.target.value }))}
|
||||
className="min-h-[160px] resize-none rounded-[12px] text-[13px] leading-5"
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
|
||||
<div className="space-y-2">
|
||||
<span className="text-[12px] font-medium text-muted-foreground">
|
||||
{tx("settings.automations.fields.scheduleType", "Schedule type")}
|
||||
</span>
|
||||
<SegmentedControl
|
||||
value={draft.scheduleKind}
|
||||
options={scheduleOptions}
|
||||
onChange={(value) =>
|
||||
setDraft((prev) => ({
|
||||
...prev,
|
||||
scheduleKind: value as AutomationEditDraft["scheduleKind"],
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{!externalTrigger ? (
|
||||
<div className="space-y-2">
|
||||
<span className="text-[12px] font-medium text-muted-foreground">
|
||||
{tx("settings.automations.fields.scheduleType", "Schedule type")}
|
||||
</span>
|
||||
<SegmentedControl
|
||||
value={draft.scheduleKind}
|
||||
options={scheduleOptions}
|
||||
onChange={(value) =>
|
||||
setDraft((prev) => ({
|
||||
...prev,
|
||||
scheduleKind: value as AutomationEditDraft["scheduleKind"],
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{draft.scheduleKind === "every" ? (
|
||||
{!externalTrigger && draft.scheduleKind === "every" ? (
|
||||
<div className="grid gap-2 sm:grid-cols-[minmax(0,1fr)_10rem]">
|
||||
<label className="block space-y-1.5">
|
||||
<span className="text-[12px] font-medium text-muted-foreground">
|
||||
@@ -4174,7 +4217,7 @@ function AutomationEditDialog({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{draft.scheduleKind === "cron" ? (
|
||||
{!externalTrigger && draft.scheduleKind === "cron" ? (
|
||||
<div className="grid gap-2 sm:grid-cols-[minmax(0,1fr)_12rem]">
|
||||
<label className="block space-y-1.5">
|
||||
<span className="text-[12px] font-medium text-muted-foreground">
|
||||
@@ -4201,7 +4244,7 @@ function AutomationEditDialog({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{draft.scheduleKind === "at" ? (
|
||||
{!externalTrigger && draft.scheduleKind === "at" ? (
|
||||
<label className="block space-y-1.5">
|
||||
<span className="text-[12px] font-medium text-muted-foreground">
|
||||
{tx("settings.automations.fields.runAt", "Run at")}
|
||||
@@ -4266,7 +4309,7 @@ function AutomationDeleteDialog({
|
||||
<DialogDescription>
|
||||
{tx(
|
||||
"settings.automations.deleteDescription",
|
||||
"This removes {{name}} from the cron store. Past chat messages stay in the session.",
|
||||
"This removes {{name}} from automations. Past chat messages stay in the session.",
|
||||
{ name: job?.name || job?.id || "" },
|
||||
)}
|
||||
</DialogDescription>
|
||||
@@ -4297,6 +4340,34 @@ function AutomationDeleteDialog({
|
||||
);
|
||||
}
|
||||
|
||||
function isExternalTriggerAutomation(job: SessionAutomationJob | null): boolean {
|
||||
if (!job) return false;
|
||||
return job.kind === "external_trigger"
|
||||
|| job.payload.kind === "external_trigger"
|
||||
|| job.schedule.kind === "external";
|
||||
}
|
||||
|
||||
function automationTriggerCommand(job: SessionAutomationJob): string {
|
||||
return job.trigger?.command || job.payload.command || job.payload.message || "";
|
||||
}
|
||||
|
||||
function automationSummary(
|
||||
job: SessionAutomationJob,
|
||||
tx: (key: string, fallback: string, values?: Record<string, unknown>) => string,
|
||||
): string {
|
||||
if (isExternalTriggerAutomation(job)) {
|
||||
return automationTriggerCommand(job) || tx("settings.automations.externalTrigger", "External trigger");
|
||||
}
|
||||
return job.payload.message || tx("settings.automations.systemTask", "System-managed automation");
|
||||
}
|
||||
|
||||
function automationDetailText(
|
||||
job: SessionAutomationJob,
|
||||
tx: (key: string, fallback: string, values?: Record<string, unknown>) => string,
|
||||
): string {
|
||||
return automationSummary(job, tx);
|
||||
}
|
||||
|
||||
function automationNeedsAttention(job: SessionAutomationJob): boolean {
|
||||
return job.state.last_status === "error";
|
||||
}
|
||||
@@ -4308,6 +4379,7 @@ function automationStatusKey(
|
||||
if (job.state.pending) return "running";
|
||||
if (!job.enabled) return "paused";
|
||||
if (job.state.last_status === "error") return "failed";
|
||||
if (isExternalTriggerAutomation(job)) return "active";
|
||||
if (job.delete_after_run && !job.state.next_run_at_ms && job.state.last_status === "ok") {
|
||||
return "completed";
|
||||
}
|
||||
@@ -4371,6 +4443,7 @@ function automationEditDraftError(
|
||||
tx: (key: string, fallback: string, values?: Record<string, unknown>) => string,
|
||||
): string | null {
|
||||
if (!draft.name.trim()) return tx("settings.automations.validation.nameRequired", "Name is required.");
|
||||
if (isExternalTriggerAutomation(job)) return null;
|
||||
if (!draft.message.trim()) {
|
||||
return tx("settings.automations.validation.messageRequired", "Message is required.");
|
||||
}
|
||||
@@ -4400,6 +4473,10 @@ function automationUpdatePayloadFromDraft(
|
||||
job: SessionAutomationJob | null,
|
||||
): AutomationUpdatePayload | string {
|
||||
const name = draft.name.trim();
|
||||
if (isExternalTriggerAutomation(job)) {
|
||||
if (!name) return "invalid";
|
||||
return { name };
|
||||
}
|
||||
const message = draft.message.trim();
|
||||
if (!name || !message) return "invalid";
|
||||
const payload: AutomationUpdatePayload = { name, message };
|
||||
@@ -4517,7 +4594,7 @@ function automationSearchParts(
|
||||
const scheduleParts = automationScheduleSearchParts(job);
|
||||
if (field === "id") return [job.id];
|
||||
if (field === "name") return [job.name, job.id];
|
||||
if (field === "message") return [job.payload.message];
|
||||
if (field === "message") return [job.payload.message, job.payload.command, job.trigger?.command];
|
||||
if (field === "chat") return originParts;
|
||||
if (field === "cron" || field === "schedule") return scheduleParts;
|
||||
if (field === "status") return [automationStatusKey(job), job.enabled ? "enabled" : "disabled"];
|
||||
@@ -4525,6 +4602,9 @@ function automationSearchParts(
|
||||
job.id,
|
||||
job.name,
|
||||
job.payload.message,
|
||||
job.payload.command,
|
||||
job.trigger?.command,
|
||||
isExternalTriggerAutomation(job) ? "trigger external" : null,
|
||||
...scheduleParts,
|
||||
automationStatusKey(job),
|
||||
...originParts,
|
||||
@@ -4714,6 +4794,9 @@ function formatAutomationSchedule(
|
||||
})
|
||||
: tx("settings.automations.schedule.cron", "Cron {{expr}}", { expr: job.schedule.expr });
|
||||
}
|
||||
if (isExternalTriggerAutomation(job)) {
|
||||
return tx("settings.automations.schedule.external", "External trigger");
|
||||
}
|
||||
return tx("settings.automations.schedule.custom", "Custom schedule");
|
||||
}
|
||||
|
||||
@@ -4768,6 +4851,9 @@ function formatAutomationNext(
|
||||
): string {
|
||||
if (!job.enabled) return tx("settings.automations.next.paused", "Paused");
|
||||
if (job.state.pending) return tx("settings.automations.next.pending", "Running now");
|
||||
if (isExternalTriggerAutomation(job)) {
|
||||
return tx("settings.automations.next.external", "Waiting for trigger");
|
||||
}
|
||||
if (!job.state.next_run_at_ms) return tx("settings.automations.next.none", "No next run");
|
||||
return relativeTime(job.state.next_run_at_ms);
|
||||
}
|
||||
|
||||
@@ -918,6 +918,10 @@
|
||||
"title": "Long-running goal",
|
||||
"description": "Tell the agent to treat this as a sustained multi-step goal."
|
||||
},
|
||||
"trigger": {
|
||||
"title": "Create local trigger",
|
||||
"description": "Create a CLI trigger bound to this chat session."
|
||||
},
|
||||
"help": {
|
||||
"title": "Show help",
|
||||
"description": "List available slash commands."
|
||||
|
||||
@@ -908,6 +908,10 @@
|
||||
"title": "Objetivo a largo plazo",
|
||||
"description": "Indica al agente que trate esto como un objetivo sostenido en varios pasos."
|
||||
},
|
||||
"trigger": {
|
||||
"title": "Crear trigger local",
|
||||
"description": "Crea un trigger de CLI vinculado a esta sesion de chat."
|
||||
},
|
||||
"help": {
|
||||
"title": "Mostrar ayuda",
|
||||
"description": "Lista los comandos slash disponibles."
|
||||
|
||||
@@ -908,6 +908,10 @@
|
||||
"title": "Objectif long terme",
|
||||
"description": "Demandez à l’agent de traiter ceci comme un objectif multi‑étapes durable."
|
||||
},
|
||||
"trigger": {
|
||||
"title": "Créer un trigger local",
|
||||
"description": "Crée un trigger CLI lié à cette session de chat."
|
||||
},
|
||||
"help": {
|
||||
"title": "Afficher l’aide",
|
||||
"description": "Lister les commandes slash disponibles."
|
||||
|
||||
@@ -908,6 +908,10 @@
|
||||
"title": "Tujuan jangka panjang",
|
||||
"description": "Instruksikan agen memperlakukan ini sebagai tujuan multi-langkah yang berkelanjutan."
|
||||
},
|
||||
"trigger": {
|
||||
"title": "Buat trigger lokal",
|
||||
"description": "Buat trigger CLI yang terikat ke sesi chat ini."
|
||||
},
|
||||
"help": {
|
||||
"title": "Tampilkan bantuan",
|
||||
"description": "Daftar perintah slash yang tersedia."
|
||||
|
||||
@@ -908,6 +908,10 @@
|
||||
"title": "長期目標",
|
||||
"description": "持続的な複数ステップの目標として扱うようエージェントに伝えます。"
|
||||
},
|
||||
"trigger": {
|
||||
"title": "ローカルトリガーを作成",
|
||||
"description": "このチャットセッションに紐づく CLI トリガーを作成します。"
|
||||
},
|
||||
"help": {
|
||||
"title": "ヘルプを表示",
|
||||
"description": "利用可能なスラッシュコマンドを一覧表示します。"
|
||||
|
||||
@@ -908,6 +908,10 @@
|
||||
"title": "장기 목표",
|
||||
"description": "에이전트에게 지속적인 다단계 목표로 처리하도록 지시합니다."
|
||||
},
|
||||
"trigger": {
|
||||
"title": "로컬 트리거 만들기",
|
||||
"description": "이 채팅 세션에 연결된 CLI 트리거를 만듭니다."
|
||||
},
|
||||
"help": {
|
||||
"title": "도움말 보기",
|
||||
"description": "사용 가능한 슬래시 명령을 나열합니다."
|
||||
|
||||
@@ -908,6 +908,10 @@
|
||||
"title": "Mục tiêu dài hạn",
|
||||
"description": "Yêu cầu agent xử lý đây là mục tiêu nhiều bước kéo dài."
|
||||
},
|
||||
"trigger": {
|
||||
"title": "Tạo trigger cục bộ",
|
||||
"description": "Tạo trigger CLI gắn với phiên chat này."
|
||||
},
|
||||
"help": {
|
||||
"title": "Hiển thị trợ giúp",
|
||||
"description": "Liệt kê các lệnh slash có sẵn."
|
||||
|
||||
@@ -917,6 +917,10 @@
|
||||
"title": "长期目标",
|
||||
"description": "让助手把当前请求当作需要多步骤持续推进的目标。"
|
||||
},
|
||||
"trigger": {
|
||||
"title": "创建本地触发器",
|
||||
"description": "创建绑定到当前聊天会话的 CLI 触发器。"
|
||||
},
|
||||
"help": {
|
||||
"title": "查看帮助",
|
||||
"description": "列出可用的斜杠命令。"
|
||||
|
||||
@@ -908,6 +908,10 @@
|
||||
"title": "長期目標",
|
||||
"description": "請助理把這則請求當成需要多步驟持續推進的目標。"
|
||||
},
|
||||
"trigger": {
|
||||
"title": "建立本機觸發器",
|
||||
"description": "建立綁定到目前聊天工作階段的 CLI 觸發器。"
|
||||
},
|
||||
"help": {
|
||||
"title": "查看說明",
|
||||
"description": "列出可用的斜線命令。"
|
||||
|
||||
@@ -32,7 +32,7 @@ export interface UIMediaAttachment {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface UIMessageSource { kind: "cron"; label?: string; }
|
||||
export interface UIMessageSource { kind: "cron" | "trigger" | string; label?: string; }
|
||||
|
||||
export interface UIMessage {
|
||||
id: string;
|
||||
@@ -104,8 +104,9 @@ export interface SessionAutomationJob {
|
||||
delete_after_run?: boolean;
|
||||
created_at_ms?: number | null;
|
||||
updated_at_ms?: number | null;
|
||||
kind?: "external_trigger" | "cron" | string;
|
||||
schedule: {
|
||||
kind: "at" | "every" | "cron" | string;
|
||||
kind: "at" | "every" | "cron" | "external" | string;
|
||||
at_ms?: number | null;
|
||||
every_ms?: number | null;
|
||||
expr?: string | null;
|
||||
@@ -113,7 +114,8 @@ export interface SessionAutomationJob {
|
||||
};
|
||||
payload: {
|
||||
message: string;
|
||||
kind?: "agent_turn" | "system_event" | string;
|
||||
kind?: "agent_turn" | "system_event" | "external_trigger" | string;
|
||||
command?: string;
|
||||
};
|
||||
state: {
|
||||
next_run_at_ms?: number | null;
|
||||
@@ -135,6 +137,10 @@ export interface SessionAutomationJob {
|
||||
title?: string;
|
||||
preview?: string;
|
||||
} | null;
|
||||
trigger?: {
|
||||
id: string;
|
||||
command: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SessionAutomationsPayload { jobs: SessionAutomationJob[]; }
|
||||
|
||||
@@ -21,6 +21,7 @@ const SLASH_COMMAND_KEYS = [
|
||||
"dream_log",
|
||||
"dream_restore",
|
||||
"goal",
|
||||
"trigger",
|
||||
"help",
|
||||
"pairing",
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user