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