palmier 0.7.8 → 0.7.9

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.
Files changed (40) hide show
  1. package/README.md +1 -1
  2. package/dist/mcp-tools.d.ts +2 -0
  3. package/dist/mcp-tools.js +4 -2
  4. package/dist/platform/linux.js +11 -8
  5. package/dist/platform/windows.d.ts +5 -6
  6. package/dist/platform/windows.js +15 -12
  7. package/dist/pwa/assets/index-FP1Mipr6.js +120 -0
  8. package/dist/pwa/assets/index-bLTn8zBj.css +1 -0
  9. package/dist/pwa/assets/{web-BNr628AV.js → web-BpM3fNCn.js} +1 -1
  10. package/dist/pwa/assets/{web-DyQPewAi.js → web-CF-N8Di6.js} +1 -1
  11. package/dist/pwa/index.html +2 -2
  12. package/dist/pwa/service-worker.js +1 -1
  13. package/dist/rpc-handler.js +23 -8
  14. package/dist/task.js +1 -1
  15. package/dist/transports/http-transport.js +3 -5
  16. package/dist/types.d.ts +9 -6
  17. package/package.json +1 -1
  18. package/palmier-server/README.md +3 -3
  19. package/palmier-server/pwa/src/App.css +117 -36
  20. package/palmier-server/pwa/src/components/PullToRefreshIndicator.tsx +46 -0
  21. package/palmier-server/pwa/src/components/RunDetailView.tsx +58 -15
  22. package/palmier-server/pwa/src/components/SessionComposer.tsx +20 -10
  23. package/palmier-server/pwa/src/components/{RunsView.tsx → SessionsView.tsx} +33 -25
  24. package/palmier-server/pwa/src/components/TaskCard.tsx +33 -35
  25. package/palmier-server/pwa/src/components/TaskForm.tsx +274 -293
  26. package/palmier-server/pwa/src/components/{TaskListView.tsx → TasksView.tsx} +20 -13
  27. package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +16 -8
  28. package/palmier-server/pwa/src/hooks/usePullToRefresh.ts +102 -0
  29. package/palmier-server/pwa/src/pages/Dashboard.tsx +9 -26
  30. package/palmier-server/pwa/src/types.ts +5 -9
  31. package/palmier-server/spec.md +23 -23
  32. package/src/mcp-tools.ts +6 -2
  33. package/src/platform/linux.ts +10 -8
  34. package/src/platform/windows.ts +15 -12
  35. package/src/rpc-handler.ts +26 -10
  36. package/src/task.ts +1 -1
  37. package/src/transports/http-transport.ts +3 -5
  38. package/src/types.ts +9 -7
  39. package/dist/pwa/assets/index-8cTctVnD.js +0 -120
  40. package/dist/pwa/assets/index-CSUkBBsQ.css +0 -1
@@ -1,16 +1,24 @@
1
- import { useState, useCallback, useRef } from "react";
1
+ import { useEffect, useState, useCallback, useRef } from "react";
2
2
  import { useHostConnection } from "../contexts/HostConnectionContext";
3
3
  import PlanDialog from "./PlanDialog";
4
4
  import { useBackClose } from "../hooks/useBackClose";
5
- import type { AgentInfo, Task, Trigger } from "../types";
5
+ import type { AgentInfo, Task } from "../types";
6
+
7
+ type ScheduleType = "crons" | "specific_times";
6
8
 
7
9
 
8
10
  const DAYS_OF_WEEK = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
9
11
 
10
- type Schedule = "once" | "hourly" | "daily" | "weekly" | "monthly";
12
+ type ScheduleSlot = "specific_times" | "hourly" | "daily" | "weekly" | "monthly";
13
+ type ScheduleMode = "ondemand" | "command" | ScheduleSlot;
14
+
15
+ const SCHEDULE_SLOTS: readonly ScheduleMode[] = ["specific_times", "hourly", "daily", "weekly", "monthly"];
16
+ function isScheduleSlot(mode: ScheduleMode): mode is ScheduleSlot {
17
+ return (SCHEDULE_SLOTS as readonly string[]).includes(mode);
18
+ }
11
19
 
12
20
  interface TriggerRow {
13
- schedule: Schedule;
21
+ schedule: ScheduleSlot;
14
22
  time: string;
15
23
  dayOfWeek: string;
16
24
  dayOfMonth: string;
@@ -18,7 +26,7 @@ interface TriggerRow {
18
26
  onceTime: string;
19
27
  }
20
28
 
21
- function newRow(schedule: Schedule = "daily"): TriggerRow {
29
+ function newRow(schedule: ScheduleSlot = "daily"): TriggerRow {
22
30
  return { schedule, time: "00:00", dayOfWeek: "1", dayOfMonth: "1", onceDate: "", onceTime: "00:00" };
23
31
  }
24
32
 
@@ -33,12 +41,12 @@ function cronToRow(cron: string): TriggerRow {
33
41
  return { ...newRow("daily"), time };
34
42
  }
35
43
 
36
- function triggerToRow(t: Trigger): TriggerRow {
37
- if (t.type === "once") {
38
- const [datePart, timePart] = t.value.split("T");
39
- return { ...newRow("once"), onceDate: datePart ?? "", onceTime: (timePart ?? "09:00").slice(0, 5) };
44
+ function valueToRow(scheduleType: ScheduleType, value: string): TriggerRow {
45
+ if (scheduleType === "specific_times") {
46
+ const [datePart, timePart] = value.split("T");
47
+ return { ...newRow("specific_times"), onceDate: datePart ?? "", onceTime: (timePart ?? "09:00").slice(0, 5) };
40
48
  }
41
- return cronToRow(t.value);
49
+ return cronToRow(value);
42
50
  }
43
51
 
44
52
  function rowToCron(row: TriggerRow): string {
@@ -52,11 +60,23 @@ function rowToCron(row: TriggerRow): string {
52
60
  }
53
61
  }
54
62
 
55
- function rowToTrigger(row: TriggerRow): Trigger | null {
56
- if (row.schedule === "once") {
57
- return row.onceDate ? { type: "once", value: `${row.onceDate}T${row.onceTime}` } : null;
63
+ function rowToValue(row: TriggerRow): string | null {
64
+ if (row.schedule === "specific_times") {
65
+ return row.onceDate ? `${row.onceDate}T${row.onceTime}` : null;
58
66
  }
59
- return { type: "cron", value: rowToCron(row) };
67
+ return rowToCron(row);
68
+ }
69
+
70
+ function modeToScheduleType(mode: ScheduleSlot): ScheduleType {
71
+ return mode === "specific_times" ? "specific_times" : "crons";
72
+ }
73
+
74
+ function initialScheduleMode(initial?: Task): ScheduleMode {
75
+ if (initial?.command) return "command";
76
+ const type = initial?.schedule_type;
77
+ const first = initial?.schedule_values?.[0];
78
+ if (!type || !first) return "ondemand";
79
+ return valueToRow(type, first).schedule;
60
80
  }
61
81
 
62
82
  interface TaskFormProps {
@@ -64,13 +84,13 @@ interface TaskFormProps {
64
84
  agents: AgentInfo[];
65
85
  hostPlatform?: string;
66
86
  onSaved(task: Task): void;
87
+ onRun(taskId: string, runId?: string): void;
67
88
  onCancel(): void;
68
89
  }
69
90
 
70
- export default function TaskForm({ initial, agents, hostPlatform, onSaved, onCancel }: TaskFormProps) {
91
+ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun, onCancel }: TaskFormProps) {
71
92
  const { request } = useHostConnection();
72
93
 
73
- // Default agent: last used from localStorage, or first available
74
94
  const defaultAgent = () => {
75
95
  const lastAgent = localStorage.getItem("palmier:lastAgent");
76
96
  const agentKeys = agents.map((a) => a.key);
@@ -78,92 +98,112 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onCan
78
98
  return agents[0]?.key ?? "";
79
99
  };
80
100
 
81
- // Editable prompt
82
101
  const [userPrompt, setUserPrompt] = useState(initial?.user_prompt ?? "");
83
102
  const [agent, setAgent] = useState(initial?.agent ?? defaultAgent());
84
103
 
85
- // Show permissions link for existing tasks that have granted permissions
86
104
  const hasPermissions = !!initial?.permissions?.length;
87
105
 
88
- // Plan dialog (view-only for existing tasks)
89
106
  const [planDialogOpen, setPlanDialogOpen] = useState(false);
90
107
  const closePlanDialog = useCallback(() => setPlanDialogOpen(false), []);
91
108
  useBackClose(planDialogOpen, closePlanDialog);
92
109
  const [error, setError] = useState<string | null>(null);
93
110
 
94
- // Form state
111
+ const [scheduleMode, setScheduleMode] = useState<ScheduleMode>(() => initialScheduleMode(initial));
95
112
  const [triggerRows, setTriggerRows] = useState<TriggerRow[]>(
96
- () => (initial?.triggers ?? []).map(triggerToRow)
97
- );
98
- const [schedule, setSchedule] = useState<Schedule>(
99
- () => (initial?.triggers ?? []).map(triggerToRow)[0]?.schedule ?? "daily"
100
- );
101
- const [triggersEnabled, setTriggersEnabled] = useState(
102
- initial?.triggers_enabled ?? false
113
+ () => {
114
+ const type = initial?.schedule_type;
115
+ const values = initial?.schedule_values;
116
+ if (!type || !values) return [];
117
+ return values.map((v) => valueToRow(type, v));
118
+ }
103
119
  );
104
120
  const [requiresConfirmation, setRequiresConfirmation] = useState(
105
121
  initial?.requires_confirmation ?? false
106
122
  );
107
123
  const [yoloMode, setYoloMode] = useState(initial?.yolo_mode ?? false);
108
124
  const [foregroundMode, setForegroundMode] = useState(initial?.foreground_mode ?? false);
125
+ const [command, setCommand] = useState(initial?.command ?? "");
109
126
  const [saving, setSaving] = useState(false);
110
127
 
111
- // Command-triggered mode
112
- const [commandEnabled, setCommandEnabled] = useState(!!initial?.command);
113
- const [command, setCommand] = useState(initial?.command ?? "");
114
128
  const commandInputRef = useRef<HTMLInputElement>(null);
115
129
 
130
+ const modeIsScheduled = isScheduleSlot(scheduleMode);
131
+ const modeIsCommand = scheduleMode === "command";
132
+
133
+ const selectedAgent = agents.find((a) => a.key === agent);
134
+ const agentSupportsYolo = !!selectedAgent?.supportsYolo;
135
+
136
+ // Force-disable yolo when the selected agent doesn't support it.
137
+ useEffect(() => {
138
+ if (!agentSupportsYolo && yoloMode) setYoloMode(false);
139
+ }, [agentSupportsYolo, yoloMode]);
140
+
116
141
  const isEdit = !!initial;
142
+ const initialMode = initialScheduleMode(initial);
117
143
  const isDirty = !isEdit
118
144
  || userPrompt !== (initial?.user_prompt ?? "")
119
145
  || agent !== (initial?.agent ?? "")
120
- || triggersEnabled !== (initial?.triggers_enabled ?? true)
146
+ || scheduleMode !== initialMode
121
147
  || requiresConfirmation !== (initial?.requires_confirmation ?? false)
122
148
  || yoloMode !== (initial?.yolo_mode ?? false)
123
149
  || foregroundMode !== (initial?.foreground_mode ?? false)
124
- || commandEnabled !== !!initial?.command
125
- || (commandEnabled && command !== (initial?.command ?? ""))
126
- || JSON.stringify(collectTriggers()) !== JSON.stringify(initial?.triggers ?? []);
127
- const hasInvalidTrigger = triggersEnabled && triggerRows.some((r) =>
128
- r.schedule === "once" && (!r.onceDate || new Date(`${r.onceDate}T${r.onceTime}`) <= new Date())
150
+ || (modeIsCommand && command !== (initial?.command ?? ""))
151
+ || (modeIsScheduled && (
152
+ JSON.stringify(collectScheduleValues()) !== JSON.stringify(initial?.schedule_values ?? [])
153
+ || modeToScheduleType(scheduleMode as ScheduleSlot) !== (initial?.schedule_type ?? undefined)
154
+ ));
155
+
156
+ const hasInvalidTrigger = modeIsScheduled && triggerRows.some((r) =>
157
+ r.schedule === "specific_times" && (!r.onceDate || new Date(`${r.onceDate}T${r.onceTime}`) <= new Date())
129
158
  );
130
- const canSave = isDirty && !!userPrompt.trim() && !hasInvalidTrigger && (!commandEnabled || !!command.trim());
159
+ const canSave = isDirty
160
+ && !!userPrompt.trim()
161
+ && !hasInvalidTrigger
162
+ && (!modeIsCommand || !!command.trim())
163
+ && (!modeIsScheduled || triggerRows.length > 0);
131
164
 
132
165
  function updateRow(index: number, patch: Partial<TriggerRow>) {
133
166
  setTriggerRows((prev) => prev.map((r, i) => (i === index ? { ...r, ...patch } : r)));
134
167
  }
135
168
 
136
169
  function removeRow(index: number) {
137
- setTriggerRows((prev) => {
138
- const next = prev.filter((_, i) => i !== index);
139
- if (next.length === 0) {
140
- setTriggersEnabled(false);
141
- setRequiresConfirmation(false);
142
- }
143
- return next;
144
- });
170
+ setTriggerRows((prev) => prev.filter((_, i) => i !== index));
145
171
  }
146
172
 
147
173
  function addRow() {
148
- setTriggerRows((prev) => [...prev, newRow(schedule)]);
174
+ if (!isScheduleSlot(scheduleMode)) return;
175
+ setTriggerRows((prev) => [...prev, newRow(scheduleMode)]);
149
176
  }
150
177
 
151
- function changeSchedule(s: Schedule) {
152
- setSchedule(s);
153
- setTriggerRows([newRow(s)]);
178
+ function changeScheduleMode(next: ScheduleMode) {
179
+ setScheduleMode(next);
180
+ if (isScheduleSlot(next)) {
181
+ setTriggerRows([newRow(next)]);
182
+ if (next !== "specific_times" && next !== "hourly" && next !== "daily" && next !== "weekly" && next !== "monthly") {
183
+ // unreachable — appeases exhaustiveness
184
+ }
185
+ } else {
186
+ setTriggerRows([]);
187
+ setRequiresConfirmation(false);
188
+ }
189
+ if (next === "command") {
190
+ setTimeout(() => commandInputRef.current?.focus(), 0);
191
+ } else {
192
+ setCommand("");
193
+ }
154
194
  }
155
195
 
156
- function collectTriggers(): Trigger[] {
196
+ function collectScheduleValues(): string[] {
157
197
  return triggerRows.flatMap((row) => {
158
- const t = rowToTrigger(row);
159
- return t ? [t] : [];
198
+ const v = rowToValue(row);
199
+ return v ? [v] : [];
160
200
  });
161
201
  }
162
202
 
163
203
  function confirmYolo(): boolean {
164
204
  if (!yoloMode) return true;
165
205
  return confirm(
166
- "Yolo mode is enabled. The agent will auto-approve all tool calls it can read, write, delete files, run arbitrary commands, and access the network without asking for permission.\n\nAre you sure you want to continue?"
206
+ "Yolo mode is enabled. The agent will auto-approve all tool calls \u2014 it can read, write, delete files, run arbitrary commands, and access the network without asking for permission.\n\nAre you sure you want to continue?"
167
207
  );
168
208
  }
169
209
 
@@ -171,47 +211,61 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onCan
171
211
  setSaving(true);
172
212
  setError(null);
173
213
  try {
174
- const method = isEdit ? "task.update" : "task.create";
214
+ const scheduleValues = modeIsScheduled ? collectScheduleValues() : [];
215
+ const hasSchedule = scheduleValues.length > 0;
175
216
  const payload: Record<string, unknown> = {
176
217
  user_prompt: userPrompt,
177
218
  agent,
178
- triggers: collectTriggers(),
179
- triggers_enabled: triggersEnabled,
180
- requires_confirmation: requiresConfirmation,
219
+ schedule_type: hasSchedule ? modeToScheduleType(scheduleMode as ScheduleSlot) : null,
220
+ schedule_values: hasSchedule ? scheduleValues : null,
221
+ schedule_enabled: hasSchedule,
222
+ requires_confirmation: hasSchedule ? requiresConfirmation : false,
181
223
  yolo_mode: yoloMode,
182
224
  foreground_mode: foregroundMode,
183
- command: commandEnabled ? command : "",
225
+ command: modeIsCommand ? command : "",
184
226
  };
185
227
  if (isEdit) {
186
228
  payload.id = initial!.id;
187
229
  }
188
- // Name generation happens server-side, allow up to 45s
230
+ const method = isEdit ? "task.update" : "task.create";
189
231
  const result = await request<Task & { error?: string }>(method, payload, { timeout: 45000 });
190
232
  if (result.error) {
191
233
  setError(result.error);
192
- return null;
234
+ return;
193
235
  }
194
- if (!isEdit) {
195
- localStorage.setItem("palmier:lastAgent", agent);
236
+ if (!isEdit) localStorage.setItem("palmier:lastAgent", agent);
237
+
238
+ // Reactive on create: save the task, then start it and navigate to the run.
239
+ if (modeIsCommand && !isEdit) {
240
+ onSaved(result);
241
+ try {
242
+ const runResult = await request<{ run_id?: string; error?: string }>("task.run", { id: result.id });
243
+ if (runResult.run_id) onRun(result.id, runResult.run_id);
244
+ else if (!runResult.error) onRun(result.id);
245
+ } catch { /* task is saved; leave the user on the list if start fails */ }
246
+ return;
196
247
  }
248
+
197
249
  onSaved(result);
198
- return result;
199
250
  } catch (err) {
200
251
  setError(err instanceof Error ? err.message : String(err));
201
- return null;
202
252
  } finally {
203
253
  setSaving(false);
204
254
  }
205
255
  }
206
256
 
257
+ const saveButtonLabel = (() => {
258
+ if (isEdit) return "Save";
259
+ if (modeIsCommand) return "Run";
260
+ if (modeIsScheduled) return "Schedule";
261
+ return "Save";
262
+ })();
207
263
 
208
264
  return (
209
265
  <div className="task-form-overlay">
210
266
  <div className="task-form">
211
267
  {planDialogOpen ? (
212
- <PlanDialog
213
- permissions={initial?.permissions}
214
- />
268
+ <PlanDialog permissions={initial?.permissions} />
215
269
  ) : (<>
216
270
  <div className="task-form-header">
217
271
  <h2>{initial ? "Edit Task" : "New Task"}</h2>
@@ -224,7 +278,7 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onCan
224
278
  className="form-textarea"
225
279
  value={userPrompt}
226
280
  onChange={(e) => setUserPrompt(e.target.value)}
227
- placeholder={commandEnabled
281
+ placeholder={modeIsCommand
228
282
  ? "If the input email contains an event, create a calendar entry for it."
229
283
  : "Research today's top AI news and write a summary."}
230
284
  rows={4}
@@ -232,45 +286,76 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onCan
232
286
  />
233
287
 
234
288
  <div className="plan-actions">
235
- {hasPermissions && (
236
- <button
237
- className="btn btn-link"
238
- onClick={() => setPlanDialogOpen(true)}
239
- >
240
- Granted Permissions
241
- </button>
242
- )}
243
- <div className="agent-picker-section-inline" style={{ marginLeft: "auto" }}>
289
+ <div className="agent-picker-section-inline">
244
290
  <span className="agent-picker-label">Run with</span>
245
291
  <select
246
292
  className="form-select form-select-sm"
247
293
  value={agent}
248
294
  onChange={(e) => setAgent(e.target.value)}
295
+ disabled={saving}
249
296
  >
250
297
  {agents.map((a) => (
251
298
  <option key={a.key} value={a.key}>{a.label}</option>
252
299
  ))}
253
300
  </select>
254
301
  </div>
302
+ {agentSupportsYolo && (
303
+ <label className="yolo-inline" style={{ marginLeft: "auto" }}>
304
+ <input
305
+ type="checkbox"
306
+ checked={yoloMode}
307
+ onChange={(e) => setYoloMode(e.target.checked)}
308
+ disabled={saving}
309
+ />
310
+ Yolo
311
+ </label>
312
+ )}
313
+ {agentSupportsYolo && yoloMode && (
314
+ <p className="yolo-warning">
315
+ The agent will auto-approve all tool calls without asking for permission.
316
+ </p>
317
+ )}
318
+ {hasPermissions && (
319
+ <div className="granted-permissions-row">
320
+ <button className="btn btn-link" onClick={() => setPlanDialogOpen(true)}>
321
+ Granted Permissions
322
+ </button>
323
+ </div>
324
+ )}
255
325
  </div>
256
326
 
257
327
  <div className="toggles-group">
258
- <div className={`command-section${commandEnabled ? " command-section-active" : ""}`}>
328
+ {hostPlatform === "win32" && (
259
329
  <label className="toggle-label">
260
330
  <input
261
331
  type="checkbox"
262
- checked={commandEnabled}
263
- onChange={(e) => {
264
- setCommandEnabled(e.target.checked);
265
- if (!e.target.checked) setCommand("");
266
- else setTimeout(() => commandInputRef.current?.focus(), 0);
267
- }}
332
+ checked={foregroundMode}
333
+ onChange={(e) => setForegroundMode(e.target.checked)}
268
334
  disabled={saving}
269
335
  />
270
- Reactive
336
+ Run in the foreground (host must login to Windows)
271
337
  </label>
272
- {commandEnabled && (
273
- <>
338
+ )}
339
+
340
+ <div className="schedule-section">
341
+ <h3 className="schedule-section-title">Schedule</h3>
342
+ <select
343
+ className="form-select"
344
+ value={scheduleMode}
345
+ onChange={(e) => changeScheduleMode(e.target.value as ScheduleMode)}
346
+ disabled={saving}
347
+ >
348
+ <option value="ondemand">On Demand</option>
349
+ <option value="specific_times">Specific Times</option>
350
+ <option value="hourly">Hourly</option>
351
+ <option value="daily">Daily</option>
352
+ <option value="weekly">Weekly</option>
353
+ <option value="monthly">Monthly</option>
354
+ <option value="command">Command-triggered</option>
355
+ </select>
356
+
357
+ {modeIsCommand && (
358
+ <div className="schedule-reactive">
274
359
  <p className="command-help-text">
275
360
  Runs a command and invokes the task for each line of output.
276
361
  Use &ldquo;the input&rdquo; in your task description to reference each line.
@@ -284,229 +369,125 @@ export default function TaskForm({ initial, agents, hostPlatform, onSaved, onCan
284
369
  placeholder="gws gmail +watch --project my-project"
285
370
  disabled={saving}
286
371
  />
287
- </>
372
+ </div>
288
373
  )}
289
- </div>
290
374
 
291
- {hostPlatform === "win32" && (
292
- <label className="toggle-label">
293
- <input
294
- type="checkbox"
295
- checked={foregroundMode}
296
- onChange={(e) => setForegroundMode(e.target.checked)}
297
- disabled={saving}
298
- />
299
- Run in the foreground (host must login to Windows)
300
- </label>
301
- )}
302
-
303
- <label className="toggle-label">
304
- <input
305
- type="checkbox"
306
- checked={yoloMode}
307
- onChange={(e) => setYoloMode(e.target.checked)}
308
- disabled={saving}
309
- />
310
- Yolo mode
311
- </label>
312
- {yoloMode && (
313
- <p className="command-help-text">
314
- The agent will auto-approve all tool calls without asking for permission.
315
- </p>
316
- )}
317
-
318
- <div className="triggers-section">
319
- <label className="toggle-label">
320
- <input
321
- type="checkbox"
322
- checked={triggersEnabled}
323
- onChange={(e) => {
324
- if (e.target.checked) {
325
- setTriggersEnabled(true);
326
- if (triggerRows.length === 0) addRow();
327
- } else {
328
- setTriggersEnabled(false);
329
- setRequiresConfirmation(false);
330
- setTriggerRows((prev) => {
331
- const cleaned = prev.filter((r) => {
332
- if (r.schedule === "once") return r.onceDate && new Date(`${r.onceDate}T${r.onceTime}`) > new Date();
333
- return true;
334
- });
335
- // Remove single untouched default row so check/uncheck round-trips cleanly
336
- if (cleaned.length === 1 && cleaned[0].schedule === "daily" && cleaned[0].time === "00:00") return [];
337
- return cleaned;
338
- });
339
- }
340
- }}
341
- disabled={saving}
342
- />
343
- Enable schedule
344
- </label>
345
- <div className={`triggers-section-body${triggersEnabled ? "" : " disabled"}`}>
346
- {triggerRows.length > 0 && (
347
- <select
348
- className="form-select"
349
- value={schedule}
350
- disabled={!triggersEnabled}
351
- onChange={(e) => changeSchedule(e.target.value as Schedule)}
352
- >
353
- <option value="once">Specific Time</option>
354
- <option value="hourly">Hourly</option>
355
- <option value="daily">Daily</option>
356
- <option value="weekly">Weekly</option>
357
- <option value="monthly">Monthly</option>
358
- </select>
359
- )}
360
- {schedule !== "hourly" && triggerRows.map((row, i) => (
361
- <div key={i} className="trigger-row-card">
362
- <div className="trigger-row-content">
363
- {schedule === "daily" && (
364
- <input
365
- className="form-input"
366
- type="time"
367
- value={row.time}
368
- disabled={!triggersEnabled}
369
- onChange={(e) => updateRow(i, { time: e.target.value })}
370
- />
371
- )}
372
- {schedule === "weekly" && (
373
- <div className="trigger-row-top">
374
- <select
375
- className="form-select"
376
- value={row.dayOfWeek}
377
- disabled={!triggersEnabled}
378
- onChange={(e) => updateRow(i, { dayOfWeek: e.target.value })}
379
- >
380
- {DAYS_OF_WEEK.map((d, di) => (
381
- <option key={di} value={String(di)}>{d}</option>
382
- ))}
383
- </select>
384
- <input
385
- className="form-input"
386
- type="time"
387
- value={row.time}
388
- disabled={!triggersEnabled}
389
- onChange={(e) => updateRow(i, { time: e.target.value })}
390
- />
391
- </div>
392
- )}
393
- {schedule === "monthly" && (
394
- <div className="trigger-row-top">
395
- <select
396
- className="form-select"
397
- value={row.dayOfMonth}
398
- disabled={!triggersEnabled}
399
- onChange={(e) => updateRow(i, { dayOfMonth: e.target.value })}
400
- >
401
- {Array.from({ length: 28 }, (_, n) => n + 1).map((d) => (
402
- <option key={d} value={String(d)}>Day {d}</option>
403
- ))}
404
- </select>
375
+ {modeIsScheduled && scheduleMode !== "hourly" && (
376
+ <>
377
+ {triggerRows.map((row, i) => (
378
+ <div key={i} className="trigger-row-card">
379
+ <div className="trigger-row-content">
380
+ {scheduleMode === "daily" && (
405
381
  <input
406
382
  className="form-input"
407
383
  type="time"
408
384
  value={row.time}
409
- disabled={!triggersEnabled}
410
385
  onChange={(e) => updateRow(i, { time: e.target.value })}
411
386
  />
412
- </div>
413
- )}
414
- {schedule === "once" && (
415
- <div className="trigger-row-top">
416
- <input
417
- className="form-input"
418
- type="date"
419
- value={row.onceDate}
420
- min={new Date().toISOString().split("T")[0]}
421
- disabled={!triggersEnabled}
422
- onChange={(e) => updateRow(i, { onceDate: e.target.value })}
423
- />
424
- <input
425
- className="form-input"
426
- type="time"
427
- value={row.onceTime}
428
- min={row.onceDate === new Date().toISOString().split("T")[0]
429
- ? new Date().toTimeString().slice(0, 5)
430
- : undefined}
431
- disabled={!triggersEnabled}
432
- onChange={(e) => updateRow(i, { onceTime: e.target.value })}
433
- />
434
- </div>
387
+ )}
388
+ {scheduleMode === "weekly" && (
389
+ <div className="trigger-row-top">
390
+ <select
391
+ className="form-select"
392
+ value={row.dayOfWeek}
393
+ onChange={(e) => updateRow(i, { dayOfWeek: e.target.value })}
394
+ >
395
+ {DAYS_OF_WEEK.map((d, di) => (
396
+ <option key={di} value={String(di)}>{d}</option>
397
+ ))}
398
+ </select>
399
+ <input
400
+ className="form-input"
401
+ type="time"
402
+ value={row.time}
403
+ onChange={(e) => updateRow(i, { time: e.target.value })}
404
+ />
405
+ </div>
406
+ )}
407
+ {scheduleMode === "monthly" && (
408
+ <div className="trigger-row-top">
409
+ <select
410
+ className="form-select"
411
+ value={row.dayOfMonth}
412
+ onChange={(e) => updateRow(i, { dayOfMonth: e.target.value })}
413
+ >
414
+ {Array.from({ length: 28 }, (_, n) => n + 1).map((d) => (
415
+ <option key={d} value={String(d)}>Day {d}</option>
416
+ ))}
417
+ </select>
418
+ <input
419
+ className="form-input"
420
+ type="time"
421
+ value={row.time}
422
+ onChange={(e) => updateRow(i, { time: e.target.value })}
423
+ />
424
+ </div>
425
+ )}
426
+ {scheduleMode === "specific_times" && (
427
+ <div className="trigger-row-top">
428
+ <input
429
+ className="form-input"
430
+ type="date"
431
+ value={row.onceDate}
432
+ min={new Date().toISOString().split("T")[0]}
433
+ onChange={(e) => updateRow(i, { onceDate: e.target.value })}
434
+ />
435
+ <input
436
+ className="form-input"
437
+ type="time"
438
+ value={row.onceTime}
439
+ min={row.onceDate === new Date().toISOString().split("T")[0]
440
+ ? new Date().toTimeString().slice(0, 5)
441
+ : undefined}
442
+ onChange={(e) => updateRow(i, { onceTime: e.target.value })}
443
+ />
444
+ </div>
445
+ )}
446
+ </div>
447
+ {triggerRows.length > 1 && (
448
+ <button
449
+ className="trigger-remove-btn"
450
+ onClick={() => removeRow(i)}
451
+ title="Remove trigger"
452
+ >
453
+ &times;
454
+ </button>
435
455
  )}
436
456
  </div>
437
- {triggerRows.length > 1 && (
438
- <button
439
- className="trigger-remove-btn"
440
- onClick={() => removeRow(i)}
441
- disabled={!triggersEnabled}
442
- title="Remove trigger"
443
- >
444
- &times;
445
- </button>
446
- )}
447
- </div>
448
- ))}
449
- {triggerRows.length > 0 && schedule !== "hourly" && (
450
- <button className="trigger-add-btn" onClick={addRow} disabled={!triggersEnabled}>
457
+ ))}
458
+ <button className="trigger-add-btn" onClick={addRow}>
451
459
  + Add
452
460
  </button>
453
- )}
454
- </div>
461
+ </>
462
+ )}
463
+
464
+ {modeIsScheduled && (
465
+ <label className="toggle-label">
466
+ <input
467
+ type="checkbox"
468
+ checked={requiresConfirmation}
469
+ onChange={(e) => setRequiresConfirmation(e.target.checked)}
470
+ disabled={saving}
471
+ />
472
+ Confirm before each run
473
+ </label>
474
+ )}
455
475
  </div>
456
- {triggersEnabled && triggerRows.length > 0 && (
457
- <label className="toggle-label">
458
- <input
459
- type="checkbox"
460
- checked={requiresConfirmation}
461
- onChange={(e) => setRequiresConfirmation(e.target.checked)}
462
- disabled={saving}
463
- />
464
- Confirm before each run
465
- </label>
466
- )}
467
476
  </div>
468
477
 
469
- {!yoloMode && (() => {
470
- const selected = agents.find((a) => a.key === agent);
471
- return selected?.supportsPermissions === false && (
472
- <div className="form-warning">Palmier does not support runtime permission granting for {selected.label}. The task may fail if required permissions are not pre-configured.</div>
473
- );
474
- })()}
478
+ {!yoloMode && selectedAgent && !selectedAgent.supportsPermissions && (
479
+ <div className="form-warning">Palmier does not support runtime permission granting for {selectedAgent.label}. The task may fail if required permissions are not pre-configured.</div>
480
+ )}
475
481
 
476
482
  <div className="form-actions">
477
- {(() => {
478
- const hasSchedule = triggerRows.length > 0;
479
- const label = hasSchedule ? "Schedule" : "Save";
480
- if (!isEdit) {
481
- return (
482
- <button
483
- className="btn btn-primary"
484
- onClick={() => confirmYolo() && handleSave()}
485
- disabled={!canSave || saving}
486
- >
487
- {saving && <span className="btn-spinner" />}
488
- {label}
489
- </button>
490
- );
491
- }
492
- if (isDirty) {
493
- return (
494
- <button
495
- className="btn btn-primary"
496
- onClick={() => confirmYolo() && handleSave()}
497
- disabled={!canSave || saving}
498
- >
499
- {saving && <span className="btn-spinner" />}
500
- Save
501
- </button>
502
- );
503
- }
504
- return (
505
- <button className="btn btn-primary" disabled>
506
- Save
507
- </button>
508
- );
509
- })()}
483
+ <button
484
+ className="btn btn-primary"
485
+ onClick={() => confirmYolo() && handleSave()}
486
+ disabled={!canSave || saving}
487
+ >
488
+ {saving && <span className="btn-spinner" />}
489
+ {saveButtonLabel}
490
+ </button>
510
491
  <button
511
492
  className="btn btn-secondary"
512
493
  onClick={() => {