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.
- package/README.md +1 -1
- package/dist/mcp-tools.d.ts +2 -0
- package/dist/mcp-tools.js +4 -2
- package/dist/platform/linux.js +11 -8
- package/dist/platform/windows.d.ts +5 -6
- package/dist/platform/windows.js +15 -12
- package/dist/pwa/assets/index-FP1Mipr6.js +120 -0
- package/dist/pwa/assets/index-bLTn8zBj.css +1 -0
- package/dist/pwa/assets/{web-BNr628AV.js → web-BpM3fNCn.js} +1 -1
- package/dist/pwa/assets/{web-DyQPewAi.js → web-CF-N8Di6.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.js +23 -8
- package/dist/task.js +1 -1
- package/dist/transports/http-transport.js +3 -5
- package/dist/types.d.ts +9 -6
- package/package.json +1 -1
- package/palmier-server/README.md +3 -3
- package/palmier-server/pwa/src/App.css +117 -36
- package/palmier-server/pwa/src/components/PullToRefreshIndicator.tsx +46 -0
- package/palmier-server/pwa/src/components/RunDetailView.tsx +58 -15
- package/palmier-server/pwa/src/components/SessionComposer.tsx +20 -10
- package/palmier-server/pwa/src/components/{RunsView.tsx → SessionsView.tsx} +33 -25
- package/palmier-server/pwa/src/components/TaskCard.tsx +33 -35
- package/palmier-server/pwa/src/components/TaskForm.tsx +274 -293
- package/palmier-server/pwa/src/components/{TaskListView.tsx → TasksView.tsx} +20 -13
- package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +16 -8
- package/palmier-server/pwa/src/hooks/usePullToRefresh.ts +102 -0
- package/palmier-server/pwa/src/pages/Dashboard.tsx +9 -26
- package/palmier-server/pwa/src/types.ts +5 -9
- package/palmier-server/spec.md +23 -23
- package/src/mcp-tools.ts +6 -2
- package/src/platform/linux.ts +10 -8
- package/src/platform/windows.ts +15 -12
- package/src/rpc-handler.ts +26 -10
- package/src/task.ts +1 -1
- package/src/transports/http-transport.ts +3 -5
- package/src/types.ts +9 -7
- package/dist/pwa/assets/index-8cTctVnD.js +0 -120
- 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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
37
|
-
if (
|
|
38
|
-
const [datePart, timePart] =
|
|
39
|
-
return { ...newRow("
|
|
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(
|
|
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
|
|
56
|
-
if (row.schedule === "
|
|
57
|
-
return row.onceDate ?
|
|
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
|
|
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
|
-
|
|
111
|
+
const [scheduleMode, setScheduleMode] = useState<ScheduleMode>(() => initialScheduleMode(initial));
|
|
95
112
|
const [triggerRows, setTriggerRows] = useState<TriggerRow[]>(
|
|
96
|
-
() =>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
||
|
|
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
|
-
||
|
|
125
|
-
|| (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
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
|
-
|
|
174
|
+
if (!isScheduleSlot(scheduleMode)) return;
|
|
175
|
+
setTriggerRows((prev) => [...prev, newRow(scheduleMode)]);
|
|
149
176
|
}
|
|
150
177
|
|
|
151
|
-
function
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
196
|
+
function collectScheduleValues(): string[] {
|
|
157
197
|
return triggerRows.flatMap((row) => {
|
|
158
|
-
const
|
|
159
|
-
return
|
|
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
|
|
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
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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:
|
|
225
|
+
command: modeIsCommand ? command : "",
|
|
184
226
|
};
|
|
185
227
|
if (isEdit) {
|
|
186
228
|
payload.id = initial!.id;
|
|
187
229
|
}
|
|
188
|
-
|
|
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
|
|
234
|
+
return;
|
|
193
235
|
}
|
|
194
|
-
if (!isEdit)
|
|
195
|
-
|
|
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={
|
|
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
|
-
|
|
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
|
-
|
|
328
|
+
{hostPlatform === "win32" && (
|
|
259
329
|
<label className="toggle-label">
|
|
260
330
|
<input
|
|
261
331
|
type="checkbox"
|
|
262
|
-
checked={
|
|
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
|
-
|
|
336
|
+
Run in the foreground (host must login to Windows)
|
|
271
337
|
</label>
|
|
272
|
-
|
|
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 “the input” 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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
+
×
|
|
454
|
+
</button>
|
|
435
455
|
)}
|
|
436
456
|
</div>
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
className="trigger-remove-btn"
|
|
440
|
-
onClick={() => removeRow(i)}
|
|
441
|
-
disabled={!triggersEnabled}
|
|
442
|
-
title="Remove trigger"
|
|
443
|
-
>
|
|
444
|
-
×
|
|
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
|
-
|
|
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
|
-
|
|
471
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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={() => {
|