palmier 0.6.6 → 0.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -1
- package/dist/agents/agent-instructions.md +6 -14
- package/dist/agents/aider.js +1 -1
- package/dist/agents/claude.js +1 -1
- package/dist/agents/cline.js +1 -1
- package/dist/agents/codex.js +1 -1
- package/dist/agents/copilot.js +1 -1
- package/dist/agents/cursor.js +1 -1
- package/dist/agents/deepagents.js +1 -1
- package/dist/agents/droid.js +1 -1
- package/dist/agents/gemini.js +1 -1
- package/dist/agents/goose.js +1 -1
- package/dist/agents/hermes.js +1 -1
- package/dist/agents/kimi.js +1 -1
- package/dist/agents/kiro.js +1 -1
- package/dist/agents/openclaw.js +1 -1
- package/dist/agents/opencode.js +1 -1
- package/dist/agents/qoder.js +1 -1
- package/dist/agents/qwen.js +1 -1
- package/dist/agents/shared-prompt.d.ts +3 -2
- package/dist/agents/shared-prompt.js +6 -4
- package/dist/commands/plan-generation.md +1 -0
- package/dist/commands/run.js +4 -7
- package/dist/location-device.d.ts +8 -0
- package/dist/location-device.js +32 -0
- package/dist/mcp-handler.d.ts +8 -0
- package/dist/mcp-handler.js +110 -0
- package/dist/mcp-tools.d.ts +27 -0
- package/dist/mcp-tools.js +218 -0
- package/dist/pwa/assets/{index-DhvJN8ie.css → index-C6Lz09EY.css} +1 -1
- package/dist/pwa/assets/index-C8vJwUNi.js +118 -0
- package/dist/pwa/assets/web-6UChJFov.js +1 -0
- package/dist/pwa/assets/web-NxTETXZK.js +1 -0
- package/dist/pwa/index.html +3 -3
- package/dist/pwa/service-worker.js +2 -2
- package/dist/rpc-handler.js +20 -8
- package/dist/spawn-command.js +3 -1
- package/dist/transports/http-transport.js +60 -129
- package/package.json +1 -1
- package/palmier-server/README.md +6 -1
- package/palmier-server/package.json +7 -1
- package/palmier-server/pnpm-lock.yaml +1025 -1
- package/palmier-server/pwa/index.html +1 -1
- package/palmier-server/pwa/package.json +3 -0
- package/palmier-server/pwa/src/App.css +64 -0
- package/palmier-server/pwa/src/api.ts +8 -2
- package/palmier-server/pwa/src/components/HostMenu.tsx +102 -1
- package/palmier-server/pwa/src/components/TaskCard.tsx +36 -8
- package/palmier-server/pwa/src/components/TaskForm.tsx +63 -53
- package/palmier-server/pwa/src/components/TaskListView.tsx +94 -78
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +2 -1
- package/palmier-server/pwa/src/hooks/usePushSubscription.ts +3 -0
- package/palmier-server/pwa/src/pages/Dashboard.tsx +5 -2
- package/palmier-server/pwa/src/pages/PairHost.tsx +10 -1
- package/palmier-server/pwa/src/service-worker.ts +7 -7
- package/palmier-server/server/.env.example +4 -0
- package/palmier-server/server/package.json +1 -0
- package/palmier-server/server/src/db.ts +10 -0
- package/palmier-server/server/src/fcm.ts +74 -0
- package/palmier-server/server/src/index.ts +101 -21
- package/palmier-server/server/src/notify.ts +34 -0
- package/palmier-server/server/src/push.ts +1 -1
- package/palmier-server/server/src/routes/fcm.ts +64 -0
- package/palmier-server/server/src/routes/push.ts +6 -5
- package/palmier-server/spec.md +4 -2
- package/src/agents/agent-instructions.md +6 -14
- package/src/agents/aider.ts +1 -1
- package/src/agents/claude.ts +1 -1
- package/src/agents/cline.ts +1 -1
- package/src/agents/codex.ts +1 -1
- package/src/agents/copilot.ts +1 -1
- package/src/agents/cursor.ts +1 -1
- package/src/agents/deepagents.ts +1 -1
- package/src/agents/droid.ts +1 -1
- package/src/agents/gemini.ts +1 -1
- package/src/agents/goose.ts +1 -1
- package/src/agents/hermes.ts +1 -1
- package/src/agents/kimi.ts +1 -1
- package/src/agents/kiro.ts +1 -1
- package/src/agents/openclaw.ts +1 -1
- package/src/agents/opencode.ts +1 -1
- package/src/agents/qoder.ts +1 -1
- package/src/agents/qwen.ts +1 -1
- package/src/agents/shared-prompt.ts +7 -4
- package/src/commands/plan-generation.md +1 -0
- package/src/commands/run.ts +4 -7
- package/src/location-device.ts +35 -0
- package/src/mcp-handler.ts +133 -0
- package/src/mcp-tools.ts +253 -0
- package/src/rpc-handler.ts +21 -8
- package/src/spawn-command.ts +3 -1
- package/src/transports/http-transport.ts +57 -128
- package/test/agent-instructions.test.ts +68 -5
- package/test/fixtures/agent-instructions-snapshot.md +58 -0
- package/dist/pwa/assets/index-CXqKVvmk.js +0 -118
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
8
8
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
|
9
9
|
<title>Palmier</title>
|
|
10
|
-
<meta name="description" content="
|
|
10
|
+
<meta name="description" content="Remote control for AI agents running on your own machine. Schedule tasks, approve permissions, and get push notifications." />
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
|
@@ -1152,6 +1152,7 @@ body {
|
|
|
1152
1152
|
width: auto;
|
|
1153
1153
|
}
|
|
1154
1154
|
|
|
1155
|
+
.triggers-section-body > .form-select,
|
|
1155
1156
|
.trigger-row-card .form-select,
|
|
1156
1157
|
.trigger-row-card .form-input {
|
|
1157
1158
|
margin-bottom: 0;
|
|
@@ -1160,6 +1161,14 @@ body {
|
|
|
1160
1161
|
height: 32px;
|
|
1161
1162
|
box-sizing: border-box;
|
|
1162
1163
|
min-width: 0;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
.triggers-section-body > .form-select {
|
|
1167
|
+
width: 100%;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
.trigger-row-card .form-select,
|
|
1171
|
+
.trigger-row-card .form-input {
|
|
1163
1172
|
flex: 1;
|
|
1164
1173
|
}
|
|
1165
1174
|
|
|
@@ -1308,6 +1317,12 @@ body {
|
|
|
1308
1317
|
font-size: 1.125rem;
|
|
1309
1318
|
font-weight: 700;
|
|
1310
1319
|
letter-spacing: -0.02em;
|
|
1320
|
+
margin-bottom: var(--space-xs);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
.confirm-modal-subtitle {
|
|
1324
|
+
font-size: 0.8rem;
|
|
1325
|
+
color: var(--color-muted);
|
|
1311
1326
|
margin-bottom: var(--space-sm);
|
|
1312
1327
|
}
|
|
1313
1328
|
|
|
@@ -1681,6 +1696,55 @@ body {
|
|
|
1681
1696
|
margin-bottom: var(--space-sm);
|
|
1682
1697
|
}
|
|
1683
1698
|
|
|
1699
|
+
.drawer-toggle {
|
|
1700
|
+
display: flex;
|
|
1701
|
+
align-items: center;
|
|
1702
|
+
justify-content: space-between;
|
|
1703
|
+
gap: var(--space-sm);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
.drawer-toggle-label {
|
|
1707
|
+
font-size: 0.85rem;
|
|
1708
|
+
color: var(--color-text);
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
.toggle-switch {
|
|
1712
|
+
position: relative;
|
|
1713
|
+
width: 40px;
|
|
1714
|
+
height: 22px;
|
|
1715
|
+
border-radius: 11px;
|
|
1716
|
+
border: none;
|
|
1717
|
+
background: var(--color-border);
|
|
1718
|
+
cursor: pointer;
|
|
1719
|
+
padding: 0;
|
|
1720
|
+
transition: background 0.2s;
|
|
1721
|
+
flex-shrink: 0;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
.toggle-switch-on {
|
|
1725
|
+
background: var(--color-primary);
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
.toggle-switch-thumb {
|
|
1729
|
+
position: absolute;
|
|
1730
|
+
top: 2px;
|
|
1731
|
+
left: 2px;
|
|
1732
|
+
width: 18px;
|
|
1733
|
+
height: 18px;
|
|
1734
|
+
border-radius: 50%;
|
|
1735
|
+
background: white;
|
|
1736
|
+
transition: transform 0.2s;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
.toggle-switch-on .toggle-switch-thumb {
|
|
1740
|
+
transform: translateX(18px);
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
.toggle-switch:disabled {
|
|
1744
|
+
opacity: 0.5;
|
|
1745
|
+
cursor: not-allowed;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1684
1748
|
.drawer-footer {
|
|
1685
1749
|
margin-top: auto;
|
|
1686
1750
|
padding: var(--space-md);
|
|
@@ -1,17 +1,23 @@
|
|
|
1
|
+
import { Capacitor } from "@capacitor/core";
|
|
2
|
+
|
|
3
|
+
/** On native platforms, API calls go to the production server. On web, they're relative (same-origin). */
|
|
4
|
+
export const SERVER_URL = Capacitor.isNativePlatform() ? "https://app.palmier.me" : "";
|
|
5
|
+
|
|
1
6
|
async function request<T>(
|
|
2
7
|
method: string,
|
|
3
8
|
path: string,
|
|
4
9
|
body?: unknown,
|
|
5
10
|
token?: string
|
|
6
11
|
): Promise<T> {
|
|
7
|
-
|
|
12
|
+
const url = `${SERVER_URL}${path}`;
|
|
13
|
+
console.log(`[API] ${method} ${url}`);
|
|
8
14
|
const headers: Record<string, string> = {
|
|
9
15
|
"Content-Type": "application/json",
|
|
10
16
|
};
|
|
11
17
|
if (token) {
|
|
12
18
|
headers["Authorization"] = `Bearer ${token}`;
|
|
13
19
|
}
|
|
14
|
-
const res = await fetch(
|
|
20
|
+
const res = await fetch(url, {
|
|
15
21
|
method,
|
|
16
22
|
headers,
|
|
17
23
|
body: body != null ? JSON.stringify(body) : undefined,
|
|
@@ -1,17 +1,39 @@
|
|
|
1
1
|
import { useState, useEffect, useRef, useCallback } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
3
|
import { useNavigate } from "react-router-dom";
|
|
4
|
+
import { Capacitor, registerPlugin } from "@capacitor/core";
|
|
5
|
+
import { App as CapApp } from "@capacitor/app";
|
|
6
|
+
import { Preferences } from "@capacitor/preferences";
|
|
7
|
+
|
|
8
|
+
interface LocationPermissionResult {
|
|
9
|
+
fine: boolean;
|
|
10
|
+
background: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface LocationPermissionPlugin {
|
|
14
|
+
request(): Promise<LocationPermissionResult>;
|
|
15
|
+
check(): Promise<LocationPermissionResult>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const LocationPermission = Capacitor.isNativePlatform()
|
|
19
|
+
? registerPlugin<LocationPermissionPlugin>("LocationPermission")
|
|
20
|
+
: null;
|
|
4
21
|
import { useHostStore } from "../contexts/HostStoreContext";
|
|
5
22
|
import { useMediaQuery } from "../hooks/useMediaQuery";
|
|
6
23
|
|
|
7
24
|
/** LAN mode: PWA is served by palmier serve (marker injected into HTML). */
|
|
8
25
|
const isLanMode = !!(window as any).__PALMIER_SERVE__;
|
|
26
|
+
const isNative = Capacitor.isNativePlatform();
|
|
9
27
|
|
|
10
28
|
interface HostMenuProps {
|
|
11
29
|
daemonVersion?: string | null;
|
|
30
|
+
locationClientToken?: string | null;
|
|
31
|
+
activeClientToken?: string | null;
|
|
32
|
+
request?<T = unknown>(method: string, params?: unknown): Promise<T>;
|
|
33
|
+
onLocationClientTokenChange?(token: string | null): void;
|
|
12
34
|
}
|
|
13
35
|
|
|
14
|
-
export default function HostMenu({ daemonVersion }: HostMenuProps) {
|
|
36
|
+
export default function HostMenu({ daemonVersion, locationClientToken, activeClientToken, request, onLocationClientTokenChange }: HostMenuProps) {
|
|
15
37
|
const { pairedHosts, activeHostId, setActiveHostId, removePairedHost, renamePairedHost } = useHostStore();
|
|
16
38
|
const navigate = useNavigate();
|
|
17
39
|
const isDesktop = useMediaQuery("(min-width: 768px)");
|
|
@@ -21,6 +43,65 @@ export default function HostMenu({ daemonVersion }: HostMenuProps) {
|
|
|
21
43
|
const [renamingId, setRenamingId] = useState<string | null>(null);
|
|
22
44
|
const [renameValue, setRenameValue] = useState("");
|
|
23
45
|
const [confirmingDeleteId, setConfirmingDeleteId] = useState<string | null>(null);
|
|
46
|
+
const [togglingLocation, setTogglingLocation] = useState(false);
|
|
47
|
+
|
|
48
|
+
const locationEnabled = !!(activeClientToken && locationClientToken === activeClientToken);
|
|
49
|
+
|
|
50
|
+
// Sync location toggle with permission state — on mount and when app resumes from background
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!isNative || !LocationPermission || !request) return;
|
|
53
|
+
|
|
54
|
+
function syncPermissionState() {
|
|
55
|
+
if (!locationEnabled) return;
|
|
56
|
+
LocationPermission!.check().then(({ fine }) => {
|
|
57
|
+
if (!fine) {
|
|
58
|
+
// Permission revoked — disable on host
|
|
59
|
+
request!("device.location.disable").then(() => {
|
|
60
|
+
onLocationClientTokenChange?.(null);
|
|
61
|
+
}).catch(() => {});
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
syncPermissionState();
|
|
67
|
+
|
|
68
|
+
const listener = CapApp.addListener("resume", () => {
|
|
69
|
+
syncPermissionState();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return () => { listener.then((h) => h.remove()); };
|
|
73
|
+
}, [locationEnabled, activeClientToken]);
|
|
74
|
+
|
|
75
|
+
async function handleLocationToggle() {
|
|
76
|
+
if (!request) return;
|
|
77
|
+
setTogglingLocation(true);
|
|
78
|
+
try {
|
|
79
|
+
if (locationEnabled) {
|
|
80
|
+
await request("device.location.disable");
|
|
81
|
+
onLocationClientTokenChange?.(null);
|
|
82
|
+
} else {
|
|
83
|
+
// Request location permissions before enabling
|
|
84
|
+
if (LocationPermission) {
|
|
85
|
+
const result = await LocationPermission.request();
|
|
86
|
+
if (!result.fine) {
|
|
87
|
+
return; // User denied permission
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { value: fcmToken } = await Preferences.get({ key: "fcmToken" });
|
|
92
|
+
if (!fcmToken) {
|
|
93
|
+
console.warn("No FCM token available");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
await request("device.location.enable", { fcmToken });
|
|
97
|
+
onLocationClientTokenChange?.(activeClientToken ?? null);
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.error("Failed to toggle location:", err);
|
|
101
|
+
} finally {
|
|
102
|
+
setTogglingLocation(false);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
24
105
|
const drawerRef = useRef<HTMLDivElement>(null);
|
|
25
106
|
const renameInputRef = useRef<HTMLInputElement>(null);
|
|
26
107
|
|
|
@@ -197,6 +278,26 @@ export default function HostMenu({ daemonVersion }: HostMenuProps) {
|
|
|
197
278
|
</div>
|
|
198
279
|
</>)}
|
|
199
280
|
|
|
281
|
+
{isNative && (
|
|
282
|
+
<>
|
|
283
|
+
<div className="drawer-divider" />
|
|
284
|
+
<div className="drawer-section">
|
|
285
|
+
<label className="drawer-toggle">
|
|
286
|
+
<span className="drawer-toggle-label">Location Access</span>
|
|
287
|
+
<button
|
|
288
|
+
className={`toggle-switch ${locationEnabled ? "toggle-switch-on" : ""}`}
|
|
289
|
+
onClick={handleLocationToggle}
|
|
290
|
+
disabled={togglingLocation}
|
|
291
|
+
role="switch"
|
|
292
|
+
aria-checked={locationEnabled}
|
|
293
|
+
>
|
|
294
|
+
<span className="toggle-switch-thumb" />
|
|
295
|
+
</button>
|
|
296
|
+
</label>
|
|
297
|
+
</div>
|
|
298
|
+
</>
|
|
299
|
+
)}
|
|
300
|
+
|
|
200
301
|
<div className="drawer-footer">
|
|
201
302
|
{daemonVersion && (
|
|
202
303
|
<div className="drawer-version">
|
|
@@ -99,25 +99,53 @@ export default function TaskCard({ task, lastEvent, onEdit, onDelete, onViewRun
|
|
|
99
99
|
};
|
|
100
100
|
|
|
101
101
|
|
|
102
|
-
function
|
|
102
|
+
function formatTriggersGrouped(triggers: { type: string; value: string }[]): string {
|
|
103
|
+
if (triggers.length === 0) return "";
|
|
104
|
+
if (triggers.length === 1) return formatSingleTrigger(triggers[0]);
|
|
105
|
+
|
|
106
|
+
// Detect the shared schedule type
|
|
107
|
+
const classified = triggers.map(classifyTrigger);
|
|
108
|
+
const types = new Set(classified.map((c) => c.kind));
|
|
109
|
+
|
|
110
|
+
// If all the same type, group them
|
|
111
|
+
if (types.size === 1) {
|
|
112
|
+
const kind = classified[0].kind;
|
|
113
|
+
if (kind === "hourly") return "Every hour";
|
|
114
|
+
const details = classified.map((c) => c.detail);
|
|
115
|
+
return `${kind.charAt(0).toUpperCase() + kind.slice(1)}: ${details.join(", ")}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Mixed types — fall back to listing each
|
|
119
|
+
return triggers.map(formatSingleTrigger).join(", ");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function classifyTrigger(t: { type: string; value: string }): { kind: string; detail: string } {
|
|
103
123
|
if (t.type === "once") {
|
|
104
124
|
const d = new Date(t.value);
|
|
105
|
-
|
|
125
|
+
const label = isNaN(d.getTime()) ? t.value : `${d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" })} at ${d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" })}`;
|
|
126
|
+
return { kind: "once", detail: label };
|
|
106
127
|
}
|
|
107
128
|
const parts = t.value.split(" ");
|
|
108
|
-
if (parts.length !== 5) return t.value;
|
|
129
|
+
if (parts.length !== 5) return { kind: "unknown", detail: t.value };
|
|
109
130
|
const [min, hour, dom, , dow] = parts;
|
|
110
131
|
const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
111
|
-
if (hour === "*") return "
|
|
132
|
+
if (hour === "*") return { kind: "hourly", detail: "" };
|
|
112
133
|
const d = new Date();
|
|
113
134
|
d.setHours(Number(hour), Number(min), 0, 0);
|
|
114
135
|
const time = d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
|
|
115
|
-
if (dow !== "*") return
|
|
116
|
-
if (dom !== "*") return
|
|
117
|
-
return
|
|
136
|
+
if (dow !== "*") return { kind: "weekly", detail: `${DAYS[Number(dow)] ?? dow} at ${time}` };
|
|
137
|
+
if (dom !== "*") return { kind: "monthly", detail: `day ${dom} at ${time}` };
|
|
138
|
+
return { kind: "daily", detail: time };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function formatSingleTrigger(t: { type: string; value: string }): string {
|
|
142
|
+
const c = classifyTrigger(t);
|
|
143
|
+
if (c.kind === "hourly") return "Every hour";
|
|
144
|
+
if (c.kind === "once") return `Once on ${c.detail}`;
|
|
145
|
+
return `${c.kind.charAt(0).toUpperCase() + c.kind.slice(1)}: ${c.detail}`;
|
|
118
146
|
}
|
|
119
147
|
|
|
120
|
-
const triggersText = task.triggers
|
|
148
|
+
const triggersText = formatTriggersGrouped(task.triggers);
|
|
121
149
|
|
|
122
150
|
const actionItems = (
|
|
123
151
|
<>
|
|
@@ -104,6 +104,9 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
|
|
|
104
104
|
const [triggerRows, setTriggerRows] = useState<TriggerRow[]>(
|
|
105
105
|
() => (initial?.triggers ?? []).map(triggerToRow)
|
|
106
106
|
);
|
|
107
|
+
const [schedule, setSchedule] = useState<Schedule>(
|
|
108
|
+
() => (initial?.triggers ?? []).map(triggerToRow)[0]?.schedule ?? "daily"
|
|
109
|
+
);
|
|
107
110
|
const [triggersEnabled, setTriggersEnabled] = useState(
|
|
108
111
|
initial?.triggers_enabled ?? false
|
|
109
112
|
);
|
|
@@ -152,7 +155,12 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
|
|
|
152
155
|
}
|
|
153
156
|
|
|
154
157
|
function addRow() {
|
|
155
|
-
setTriggerRows((prev) => [...prev, newRow(
|
|
158
|
+
setTriggerRows((prev) => [...prev, newRow(schedule)]);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function changeSchedule(s: Schedule) {
|
|
162
|
+
setSchedule(s);
|
|
163
|
+
setTriggerRows([newRow(s)]);
|
|
156
164
|
}
|
|
157
165
|
|
|
158
166
|
function collectTriggers(): Trigger[] {
|
|
@@ -370,26 +378,47 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
|
|
|
370
378
|
}}
|
|
371
379
|
disabled={saving}
|
|
372
380
|
/>
|
|
373
|
-
|
|
381
|
+
Enable schedule
|
|
374
382
|
</label>
|
|
375
383
|
<div className={`triggers-section-body${triggersEnabled ? "" : " disabled"}`}>
|
|
376
|
-
{triggerRows.
|
|
384
|
+
{triggerRows.length > 0 && (
|
|
385
|
+
<select
|
|
386
|
+
className="form-select"
|
|
387
|
+
value={schedule}
|
|
388
|
+
disabled={!triggersEnabled}
|
|
389
|
+
onChange={(e) => changeSchedule(e.target.value as Schedule)}
|
|
390
|
+
>
|
|
391
|
+
<option value="once">Specific Time</option>
|
|
392
|
+
<option value="hourly">Hourly</option>
|
|
393
|
+
<option value="daily">Daily</option>
|
|
394
|
+
<option value="weekly">Weekly</option>
|
|
395
|
+
<option value="monthly">Monthly</option>
|
|
396
|
+
</select>
|
|
397
|
+
)}
|
|
398
|
+
{schedule !== "hourly" && triggerRows.map((row, i) => (
|
|
377
399
|
<div key={i} className="trigger-row-card">
|
|
378
400
|
<div className="trigger-row-content">
|
|
379
|
-
|
|
380
|
-
<
|
|
381
|
-
className="form-
|
|
382
|
-
|
|
401
|
+
{schedule === "daily" && (
|
|
402
|
+
<input
|
|
403
|
+
className="form-input"
|
|
404
|
+
type="time"
|
|
405
|
+
value={row.time}
|
|
383
406
|
disabled={!triggersEnabled}
|
|
384
|
-
onChange={(e) => updateRow(i, {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
<
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
407
|
+
onChange={(e) => updateRow(i, { time: e.target.value })}
|
|
408
|
+
/>
|
|
409
|
+
)}
|
|
410
|
+
{schedule === "weekly" && (
|
|
411
|
+
<div className="trigger-row-top">
|
|
412
|
+
<select
|
|
413
|
+
className="form-select"
|
|
414
|
+
value={row.dayOfWeek}
|
|
415
|
+
disabled={!triggersEnabled}
|
|
416
|
+
onChange={(e) => updateRow(i, { dayOfWeek: e.target.value })}
|
|
417
|
+
>
|
|
418
|
+
{DAYS_OF_WEEK.map((d, di) => (
|
|
419
|
+
<option key={di} value={String(di)}>{d}</option>
|
|
420
|
+
))}
|
|
421
|
+
</select>
|
|
393
422
|
<input
|
|
394
423
|
className="form-input"
|
|
395
424
|
type="time"
|
|
@@ -397,31 +426,10 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
|
|
|
397
426
|
disabled={!triggersEnabled}
|
|
398
427
|
onChange={(e) => updateRow(i, { time: e.target.value })}
|
|
399
428
|
/>
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
className="form-select"
|
|
405
|
-
value={row.dayOfWeek}
|
|
406
|
-
disabled={!triggersEnabled}
|
|
407
|
-
onChange={(e) => updateRow(i, { dayOfWeek: e.target.value })}
|
|
408
|
-
>
|
|
409
|
-
{DAYS_OF_WEEK.map((d, di) => (
|
|
410
|
-
<option key={di} value={String(di)}>{d}</option>
|
|
411
|
-
))}
|
|
412
|
-
</select>
|
|
413
|
-
<input
|
|
414
|
-
className="form-input"
|
|
415
|
-
type="time"
|
|
416
|
-
value={row.time}
|
|
417
|
-
disabled={!triggersEnabled}
|
|
418
|
-
onChange={(e) => updateRow(i, { time: e.target.value })}
|
|
419
|
-
/>
|
|
420
|
-
</>
|
|
421
|
-
)}
|
|
422
|
-
</div>
|
|
423
|
-
{row.schedule === "monthly" && (
|
|
424
|
-
<div className="trigger-details">
|
|
429
|
+
</div>
|
|
430
|
+
)}
|
|
431
|
+
{schedule === "monthly" && (
|
|
432
|
+
<div className="trigger-row-top">
|
|
425
433
|
<select
|
|
426
434
|
className="form-select"
|
|
427
435
|
value={row.dayOfMonth}
|
|
@@ -441,8 +449,8 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
|
|
|
441
449
|
/>
|
|
442
450
|
</div>
|
|
443
451
|
)}
|
|
444
|
-
{
|
|
445
|
-
<div className="trigger-
|
|
452
|
+
{schedule === "once" && (
|
|
453
|
+
<div className="trigger-row-top">
|
|
446
454
|
<input
|
|
447
455
|
className="form-input"
|
|
448
456
|
type="date"
|
|
@@ -464,19 +472,21 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun
|
|
|
464
472
|
</div>
|
|
465
473
|
)}
|
|
466
474
|
</div>
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
+
{triggerRows.length > 1 && (
|
|
476
|
+
<button
|
|
477
|
+
className="trigger-remove-btn"
|
|
478
|
+
onClick={() => removeRow(i)}
|
|
479
|
+
disabled={!triggersEnabled}
|
|
480
|
+
title="Remove trigger"
|
|
481
|
+
>
|
|
482
|
+
×
|
|
483
|
+
</button>
|
|
484
|
+
)}
|
|
475
485
|
</div>
|
|
476
486
|
))}
|
|
477
|
-
{triggerRows.length > 0 && (
|
|
487
|
+
{triggerRows.length > 0 && schedule !== "hourly" && (
|
|
478
488
|
<button className="trigger-add-btn" onClick={addRow} disabled={!triggersEnabled}>
|
|
479
|
-
+ Add
|
|
489
|
+
+ Add
|
|
480
490
|
</button>
|
|
481
491
|
)}
|
|
482
492
|
</div>
|