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.
- package/README.md +1 -1
- package/dist/agents/agent.d.ts +3 -0
- package/dist/agents/agent.js +1 -1
- package/dist/agents/aider.d.ts +1 -0
- package/dist/agents/aider.js +1 -0
- package/dist/agents/claude.d.ts +1 -0
- package/dist/agents/claude.js +1 -0
- package/dist/agents/cline.d.ts +1 -0
- package/dist/agents/cline.js +1 -0
- package/dist/agents/codex.d.ts +1 -0
- package/dist/agents/codex.js +1 -0
- package/dist/agents/copilot.d.ts +1 -0
- package/dist/agents/copilot.js +1 -0
- package/dist/agents/cursor.d.ts +1 -0
- package/dist/agents/cursor.js +1 -0
- package/dist/agents/deepagents.d.ts +1 -0
- package/dist/agents/deepagents.js +1 -0
- package/dist/agents/droid.d.ts +1 -0
- package/dist/agents/droid.js +1 -0
- package/dist/agents/gemini.d.ts +1 -0
- package/dist/agents/gemini.js +1 -0
- package/dist/agents/goose.d.ts +1 -0
- package/dist/agents/goose.js +1 -0
- package/dist/agents/hermes.d.ts +1 -0
- package/dist/agents/hermes.js +1 -0
- package/dist/agents/kimi.d.ts +1 -0
- package/dist/agents/kimi.js +1 -0
- package/dist/agents/kiro.d.ts +1 -0
- package/dist/agents/kiro.js +1 -0
- package/dist/agents/openclaw.d.ts +1 -0
- package/dist/agents/openclaw.js +2 -2
- package/dist/agents/opencode.d.ts +1 -0
- package/dist/agents/opencode.js +1 -0
- package/dist/agents/qoder.d.ts +1 -0
- package/dist/agents/qoder.js +1 -0
- package/dist/agents/qwen.d.ts +1 -0
- package/dist/agents/qwen.js +1 -0
- package/dist/commands/pair.js +2 -2
- package/dist/mcp-tools.d.ts +2 -0
- package/dist/mcp-tools.js +20 -9
- package/dist/pending-requests.d.ts +30 -8
- package/dist/pending-requests.js +28 -15
- 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-CkWrlNwc.js → web-BpM3fNCn.js} +1 -1
- package/dist/pwa/assets/{web-lx34oBi7.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 +35 -24
- package/dist/task.js +1 -1
- package/dist/transports/http-transport.js +9 -8
- package/dist/types.d.ts +11 -6
- package/package.json +1 -1
- package/palmier-server/README.md +3 -3
- package/palmier-server/pwa/src/App.css +175 -28
- package/palmier-server/pwa/src/App.tsx +1 -0
- package/palmier-server/pwa/src/components/HostMenu.tsx +7 -2
- 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 +147 -0
- package/palmier-server/pwa/src/components/{RunsView.tsx → SessionsView.tsx} +79 -45
- package/palmier-server/pwa/src/components/TabBar.tsx +17 -10
- package/palmier-server/pwa/src/components/TaskCard.tsx +33 -35
- package/palmier-server/pwa/src/components/TaskForm.tsx +275 -349
- package/palmier-server/pwa/src/components/TasksView.tsx +172 -0
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +16 -8
- package/palmier-server/pwa/src/draftGuard.ts +24 -0
- package/palmier-server/pwa/src/hooks/usePullToRefresh.ts +102 -0
- package/palmier-server/pwa/src/pages/Dashboard.tsx +343 -37
- package/palmier-server/pwa/src/types.ts +5 -14
- package/palmier-server/spec.md +39 -26
- package/src/agents/agent.ts +5 -1
- package/src/agents/aider.ts +1 -0
- package/src/agents/claude.ts +1 -0
- package/src/agents/cline.ts +1 -0
- package/src/agents/codex.ts +1 -0
- package/src/agents/copilot.ts +1 -0
- package/src/agents/cursor.ts +1 -0
- package/src/agents/deepagents.ts +1 -0
- package/src/agents/droid.ts +1 -0
- package/src/agents/gemini.ts +1 -0
- package/src/agents/goose.ts +1 -0
- package/src/agents/hermes.ts +1 -0
- package/src/agents/kimi.ts +1 -0
- package/src/agents/kiro.ts +1 -0
- package/src/agents/openclaw.ts +2 -2
- package/src/agents/opencode.ts +1 -0
- package/src/agents/qoder.ts +1 -0
- package/src/agents/qwen.ts +1 -0
- package/src/commands/pair.ts +2 -2
- package/src/mcp-tools.ts +22 -9
- package/src/pending-requests.ts +47 -15
- package/src/platform/linux.ts +10 -8
- package/src/platform/windows.ts +15 -12
- package/src/rpc-handler.ts +39 -26
- package/src/task.ts +1 -1
- package/src/transports/http-transport.ts +9 -8
- package/src/types.ts +10 -8
- package/test/pairing.test.ts +2 -2
- package/dist/pwa/assets/index-B-ByUHPS.css +0 -1
- package/dist/pwa/assets/index-Bt8Hhaw3.js +0 -118
- 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
|
|
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 {
|
|
@@ -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
|
-
|
|
111
|
+
const [scheduleMode, setScheduleMode] = useState<ScheduleMode>(() => initialScheduleMode(initial));
|
|
96
112
|
const [triggerRows, setTriggerRows] = useState<TriggerRow[]>(
|
|
97
|
-
() =>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
||
|
|
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
|
-
||
|
|
127
|
-
|| (
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
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
|
-
|
|
174
|
+
if (!isScheduleSlot(scheduleMode)) return;
|
|
175
|
+
setTriggerRows((prev) => [...prev, newRow(scheduleMode)]);
|
|
151
176
|
}
|
|
152
177
|
|
|
153
|
-
function
|
|
154
|
-
|
|
155
|
-
|
|
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
|
|
196
|
+
function collectScheduleValues(): string[] {
|
|
159
197
|
return triggerRows.flatMap((row) => {
|
|
160
|
-
const
|
|
161
|
-
return
|
|
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
|
|
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
|
-
|
|
211
|
+
setSaving(true);
|
|
174
212
|
setError(null);
|
|
175
213
|
try {
|
|
176
|
-
const
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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:
|
|
225
|
+
command: modeIsCommand ? command : "",
|
|
186
226
|
};
|
|
187
227
|
if (isEdit) {
|
|
188
228
|
payload.id = initial!.id;
|
|
189
229
|
}
|
|
190
|
-
|
|
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
|
|
195
|
-
}
|
|
196
|
-
if (!isEdit) {
|
|
197
|
-
localStorage.setItem("palmier:lastAgent", agent);
|
|
234
|
+
return;
|
|
198
235
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
onCancel();
|
|
248
|
+
|
|
249
|
+
onSaved(result);
|
|
229
250
|
} catch (err) {
|
|
230
251
|
setError(err instanceof Error ? err.message : String(err));
|
|
231
252
|
} finally {
|
|
232
|
-
|
|
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={
|
|
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
|
-
|
|
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
|
-
|
|
328
|
+
{hostPlatform === "win32" && (
|
|
288
329
|
<label className="toggle-label">
|
|
289
330
|
<input
|
|
290
331
|
type="checkbox"
|
|
291
|
-
checked={
|
|
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
|
-
|
|
336
|
+
Run in the foreground (host must login to Windows)
|
|
300
337
|
</label>
|
|
301
|
-
|
|
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 “the input” 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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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>
|
|
464
455
|
)}
|
|
465
456
|
</div>
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
className="trigger-remove-btn"
|
|
469
|
-
onClick={() => removeRow(i)}
|
|
470
|
-
disabled={!triggersEnabled}
|
|
471
|
-
title="Remove trigger"
|
|
472
|
-
>
|
|
473
|
-
×
|
|
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
|
-
|
|
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
|
-
|
|
500
|
-
|
|
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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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={() => {
|