palmier 0.6.0 → 0.6.2
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/.github/workflows/publish.yml +15 -2
- package/CLAUDE.md +2 -2
- package/DISCLAIMER.md +36 -0
- package/README.md +76 -87
- package/dist/agents/agent-instructions.md +1 -1
- package/dist/agents/agent.d.ts +2 -0
- package/dist/agents/agent.js +21 -0
- package/dist/agents/aider.d.ts +9 -0
- package/dist/agents/aider.js +32 -0
- package/dist/agents/cursor.d.ts +9 -0
- package/dist/agents/cursor.js +35 -0
- package/dist/agents/deepagents.d.ts +9 -0
- package/dist/agents/deepagents.js +35 -0
- package/dist/agents/droid.d.ts +9 -0
- package/dist/agents/droid.js +32 -0
- package/dist/agents/goose.d.ts +9 -0
- package/dist/agents/goose.js +32 -0
- package/dist/agents/opencode.d.ts +9 -0
- package/dist/agents/opencode.js +35 -0
- package/dist/agents/openhands.d.ts +9 -0
- package/dist/agents/openhands.js +35 -0
- package/dist/commands/pair.d.ts +1 -1
- package/dist/commands/pair.js +1 -1
- package/dist/commands/run.js +2 -2
- package/dist/pwa/apple-touch-icon.png +0 -0
- package/dist/pwa/assets/index-ByhOhTz1.js +118 -0
- package/dist/pwa/assets/index-_AmC1Rkn.css +1 -0
- package/dist/pwa/assets/plus-jakarta-sans-latin-ext-wght-normal-DmpS2jIq.woff2 +0 -0
- package/dist/pwa/assets/plus-jakarta-sans-latin-wght-normal-eXO_dkmS.woff2 +0 -0
- package/dist/pwa/assets/plus-jakarta-sans-vietnamese-wght-normal-qRpaaN48.woff2 +0 -0
- package/dist/pwa/favicon.ico +0 -0
- package/dist/pwa/index.html +17 -0
- package/dist/pwa/manifest.webmanifest +1 -0
- package/dist/pwa/pwa-192x192.png +0 -0
- package/dist/pwa/pwa-512x512.png +0 -0
- package/dist/pwa/registerSW.js +1 -0
- package/dist/pwa/service-worker.js +2 -0
- package/dist/rpc-handler.d.ts +4 -0
- package/dist/rpc-handler.js +5 -4
- package/dist/transports/http-transport.js +29 -41
- package/package.json +2 -2
- package/palmier-server/.github/workflows/ci.yml +21 -0
- package/palmier-server/.github/workflows/deploy.yml +38 -0
- package/palmier-server/CLAUDE.md +13 -0
- package/palmier-server/PRODUCTION.md +355 -0
- package/palmier-server/README.md +187 -0
- package/palmier-server/nats.conf +15 -0
- package/palmier-server/package.json +8 -0
- package/palmier-server/pnpm-lock.yaml +6597 -0
- package/palmier-server/pnpm-workspace.yaml +3 -0
- package/palmier-server/pwa/index.html +16 -0
- package/palmier-server/pwa/logo/logo-prompt.md +28 -0
- package/palmier-server/pwa/logo/logo_20260330.png +0 -0
- package/palmier-server/pwa/package.json +30 -0
- package/palmier-server/pwa/public/apple-touch-icon.png +0 -0
- package/palmier-server/pwa/public/favicon.ico +0 -0
- package/palmier-server/pwa/public/pwa-192x192.png +0 -0
- package/palmier-server/pwa/public/pwa-512x512.png +0 -0
- package/palmier-server/pwa/src/App.css +2387 -0
- package/palmier-server/pwa/src/App.tsx +21 -0
- package/palmier-server/pwa/src/agentLabels.ts +11 -0
- package/palmier-server/pwa/src/api.ts +61 -0
- package/palmier-server/pwa/src/components/HostMenu.tsx +289 -0
- package/palmier-server/pwa/src/components/PlanDialog.tsx +41 -0
- package/palmier-server/pwa/src/components/RunDetailView.tsx +293 -0
- package/palmier-server/pwa/src/components/RunsView.tsx +254 -0
- package/palmier-server/pwa/src/components/TabBar.tsx +31 -0
- package/palmier-server/pwa/src/components/TaskCard.tsx +213 -0
- package/palmier-server/pwa/src/components/TaskForm.tsx +580 -0
- package/palmier-server/pwa/src/components/TaskListView.tsx +415 -0
- package/palmier-server/pwa/src/constants.ts +2 -0
- package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +313 -0
- package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +135 -0
- package/palmier-server/pwa/src/formatTime.ts +10 -0
- package/palmier-server/pwa/src/hooks/useBackClose.ts +75 -0
- package/palmier-server/pwa/src/hooks/useMediaQuery.ts +17 -0
- package/palmier-server/pwa/src/hooks/usePushSubscription.ts +75 -0
- package/palmier-server/pwa/src/main.tsx +14 -0
- package/palmier-server/pwa/src/pages/Dashboard.tsx +223 -0
- package/palmier-server/pwa/src/pages/PairHost.tsx +178 -0
- package/palmier-server/pwa/src/service-worker.ts +139 -0
- package/palmier-server/pwa/src/types.ts +79 -0
- package/palmier-server/pwa/src/vite-env.d.ts +11 -0
- package/palmier-server/pwa/tsconfig.json +21 -0
- package/palmier-server/pwa/tsconfig.node.json +19 -0
- package/palmier-server/pwa/vite.config.ts +47 -0
- package/palmier-server/server/.env.example +16 -0
- package/palmier-server/server/package.json +33 -0
- package/palmier-server/server/src/db.ts +34 -0
- package/palmier-server/server/src/index.ts +219 -0
- package/palmier-server/server/src/nats.ts +25 -0
- package/palmier-server/server/src/push.ts +68 -0
- package/palmier-server/server/src/routes/hosts.ts +45 -0
- package/palmier-server/server/src/routes/push.ts +100 -0
- package/palmier-server/server/tsconfig.json +20 -0
- package/palmier-server/spec.md +415 -0
- package/src/agents/agent-instructions.md +1 -1
- package/src/agents/agent.ts +23 -0
- package/src/agents/aider.ts +37 -0
- package/src/agents/cursor.ts +38 -0
- package/src/agents/deepagents.ts +38 -0
- package/src/agents/droid.ts +37 -0
- package/src/agents/goose.ts +35 -0
- package/src/agents/opencode.ts +38 -0
- package/src/agents/openhands.ts +38 -0
- package/src/commands/pair.ts +1 -1
- package/src/commands/run.ts +2 -2
- package/src/rpc-handler.ts +5 -4
- package/src/transports/http-transport.ts +31 -43
- package/test/result-state.test.ts +110 -0
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
import { useState, useCallback, useRef } from "react";
|
|
2
|
+
import { useHostConnection } from "../contexts/HostConnectionContext";
|
|
3
|
+
import PlanDialog from "./PlanDialog";
|
|
4
|
+
import { useBackClose } from "../hooks/useBackClose";
|
|
5
|
+
import type { AgentInfo, Task, Trigger } from "../types";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
const DAYS_OF_WEEK = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
9
|
+
|
|
10
|
+
type Schedule = "once" | "hourly" | "daily" | "weekly" | "monthly";
|
|
11
|
+
|
|
12
|
+
interface TriggerRow {
|
|
13
|
+
schedule: Schedule;
|
|
14
|
+
time: string;
|
|
15
|
+
dayOfWeek: string;
|
|
16
|
+
dayOfMonth: string;
|
|
17
|
+
onceDate: string;
|
|
18
|
+
onceTime: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function newRow(schedule: Schedule = "daily"): TriggerRow {
|
|
22
|
+
return { schedule, time: "00:00", dayOfWeek: "1", dayOfMonth: "1", onceDate: "", onceTime: "00:00" };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function cronToRow(cron: string): TriggerRow {
|
|
26
|
+
const parts = cron.split(" ");
|
|
27
|
+
if (parts.length !== 5) return newRow();
|
|
28
|
+
const [min, hour, dom, , dow] = parts;
|
|
29
|
+
if (hour === "*") return newRow("hourly");
|
|
30
|
+
const time = `${hour.padStart(2, "0")}:${min.padStart(2, "0")}`;
|
|
31
|
+
if (dow !== "*") return { ...newRow("weekly"), time, dayOfWeek: dow };
|
|
32
|
+
if (dom !== "*") return { ...newRow("monthly"), time, dayOfMonth: dom };
|
|
33
|
+
return { ...newRow("daily"), time };
|
|
34
|
+
}
|
|
35
|
+
|
|
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) };
|
|
40
|
+
}
|
|
41
|
+
return cronToRow(t.value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function rowToCron(row: TriggerRow): string {
|
|
45
|
+
const [hh, mm] = row.time.split(":").map(Number);
|
|
46
|
+
switch (row.schedule) {
|
|
47
|
+
case "hourly": return "0 * * * *";
|
|
48
|
+
case "daily": return `${mm} ${hh} * * *`;
|
|
49
|
+
case "weekly": return `${mm} ${hh} * * ${row.dayOfWeek}`;
|
|
50
|
+
case "monthly": return `${mm} ${hh} ${row.dayOfMonth} * *`;
|
|
51
|
+
default: return "0 * * * *";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
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;
|
|
58
|
+
}
|
|
59
|
+
return { type: "cron", value: rowToCron(row) };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface TaskFormProps {
|
|
63
|
+
initial?: Task;
|
|
64
|
+
agents: AgentInfo[];
|
|
65
|
+
hostPlatform?: string;
|
|
66
|
+
onSaved(task: Task): void;
|
|
67
|
+
onRun(taskId: string, runId?: string): void;
|
|
68
|
+
onCancel(): void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default function TaskForm({ initial, agents, hostPlatform, onSaved, onRun, onCancel }: TaskFormProps) {
|
|
72
|
+
const { request } = useHostConnection();
|
|
73
|
+
|
|
74
|
+
// Default agent: last used from localStorage, or first available
|
|
75
|
+
const defaultAgent = () => {
|
|
76
|
+
const lastAgent = localStorage.getItem("palmier:lastAgent");
|
|
77
|
+
const agentKeys = agents.map((a) => a.key);
|
|
78
|
+
if (lastAgent && agentKeys.includes(lastAgent)) return lastAgent;
|
|
79
|
+
return agents[0]?.key ?? "";
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Editable prompt
|
|
83
|
+
const [userPrompt, setUserPrompt] = useState(initial?.user_prompt ?? "");
|
|
84
|
+
const [agent, setAgent] = useState(initial?.agent ?? defaultAgent());
|
|
85
|
+
|
|
86
|
+
// Plan state — for existing tasks only
|
|
87
|
+
const [body] = useState(initial?.body ?? "");
|
|
88
|
+
|
|
89
|
+
// Track whether prompt or agent diverged from the saved values (for existing tasks)
|
|
90
|
+
const promptChanged = !!initial && userPrompt !== (initial.user_prompt ?? "");
|
|
91
|
+
const agentChanged = !!initial && agent !== (initial.agent ?? "");
|
|
92
|
+
const planInvalidated = promptChanged || agentChanged;
|
|
93
|
+
|
|
94
|
+
// Show plan link for existing tasks that have a plan or permissions and haven't been modified
|
|
95
|
+
const hasPlan = !!initial && (!!body || !!initial.permissions?.length) && !planInvalidated;
|
|
96
|
+
|
|
97
|
+
// Plan dialog (view-only for existing tasks)
|
|
98
|
+
const [planDialogOpen, setPlanDialogOpen] = useState(false);
|
|
99
|
+
const closePlanDialog = useCallback(() => setPlanDialogOpen(false), []);
|
|
100
|
+
useBackClose(planDialogOpen, closePlanDialog);
|
|
101
|
+
const [error, setError] = useState<string | null>(null);
|
|
102
|
+
|
|
103
|
+
// Form state
|
|
104
|
+
const [triggerRows, setTriggerRows] = useState<TriggerRow[]>(
|
|
105
|
+
() => (initial?.triggers ?? []).map(triggerToRow)
|
|
106
|
+
);
|
|
107
|
+
const [triggersEnabled, setTriggersEnabled] = useState(
|
|
108
|
+
initial?.triggers_enabled ?? false
|
|
109
|
+
);
|
|
110
|
+
const [requiresConfirmation, setRequiresConfirmation] = useState(
|
|
111
|
+
initial?.requires_confirmation ?? false
|
|
112
|
+
);
|
|
113
|
+
const [yoloMode, setYoloMode] = useState(initial?.yolo_mode ?? false);
|
|
114
|
+
const [foregroundMode, setForegroundMode] = useState(initial?.foreground_mode ?? false);
|
|
115
|
+
const [savingAction, setSavingAction] = useState<"save" | "run" | null>(null);
|
|
116
|
+
const saving = savingAction !== null;
|
|
117
|
+
|
|
118
|
+
// Command-triggered mode
|
|
119
|
+
const [commandEnabled, setCommandEnabled] = useState(!!initial?.command);
|
|
120
|
+
const [command, setCommand] = useState(initial?.command ?? "");
|
|
121
|
+
const commandInputRef = useRef<HTMLInputElement>(null);
|
|
122
|
+
|
|
123
|
+
const isEdit = !!initial;
|
|
124
|
+
const isDirty = !isEdit
|
|
125
|
+
|| userPrompt !== (initial?.user_prompt ?? "")
|
|
126
|
+
|| agent !== (initial?.agent ?? "")
|
|
127
|
+
|| triggersEnabled !== (initial?.triggers_enabled ?? true)
|
|
128
|
+
|| requiresConfirmation !== (initial?.requires_confirmation ?? false)
|
|
129
|
+
|| yoloMode !== (initial?.yolo_mode ?? false)
|
|
130
|
+
|| foregroundMode !== (initial?.foreground_mode ?? false)
|
|
131
|
+
|| commandEnabled !== !!initial?.command
|
|
132
|
+
|| (commandEnabled && command !== (initial?.command ?? ""))
|
|
133
|
+
|| JSON.stringify(collectTriggers()) !== JSON.stringify(initial?.triggers ?? []);
|
|
134
|
+
const hasInvalidTrigger = triggersEnabled && triggerRows.some((r) =>
|
|
135
|
+
r.schedule === "once" && (!r.onceDate || new Date(`${r.onceDate}T${r.onceTime}`) <= new Date())
|
|
136
|
+
);
|
|
137
|
+
const canSave = isDirty && !!userPrompt.trim() && !hasInvalidTrigger && (!commandEnabled || !!command.trim());
|
|
138
|
+
|
|
139
|
+
function updateRow(index: number, patch: Partial<TriggerRow>) {
|
|
140
|
+
setTriggerRows((prev) => prev.map((r, i) => (i === index ? { ...r, ...patch } : r)));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function removeRow(index: number) {
|
|
144
|
+
setTriggerRows((prev) => {
|
|
145
|
+
const next = prev.filter((_, i) => i !== index);
|
|
146
|
+
if (next.length === 0) {
|
|
147
|
+
setTriggersEnabled(false);
|
|
148
|
+
setRequiresConfirmation(false);
|
|
149
|
+
}
|
|
150
|
+
return next;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function addRow() {
|
|
155
|
+
setTriggerRows((prev) => [...prev, newRow(prev.length > 0 ? prev[prev.length - 1].schedule : undefined)]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function collectTriggers(): Trigger[] {
|
|
159
|
+
return triggerRows.flatMap((row) => {
|
|
160
|
+
const t = rowToTrigger(row);
|
|
161
|
+
return t ? [t] : [];
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function confirmYolo(): boolean {
|
|
166
|
+
if (!yoloMode) return true;
|
|
167
|
+
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?"
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function handleSave() {
|
|
173
|
+
setSavingAction("save");
|
|
174
|
+
setError(null);
|
|
175
|
+
try {
|
|
176
|
+
const method = isEdit ? "task.update" : "task.create";
|
|
177
|
+
const payload: Record<string, unknown> = {
|
|
178
|
+
user_prompt: userPrompt,
|
|
179
|
+
agent,
|
|
180
|
+
triggers: collectTriggers(),
|
|
181
|
+
triggers_enabled: triggersEnabled,
|
|
182
|
+
requires_confirmation: requiresConfirmation,
|
|
183
|
+
yolo_mode: yoloMode,
|
|
184
|
+
foreground_mode: foregroundMode,
|
|
185
|
+
command: commandEnabled ? command : "",
|
|
186
|
+
};
|
|
187
|
+
if (isEdit) {
|
|
188
|
+
payload.id = initial!.id;
|
|
189
|
+
}
|
|
190
|
+
// Plan generation happens server-side, allow up to 130s
|
|
191
|
+
const result = await request<Task & { error?: string }>(method, payload, { timeout: 130000 });
|
|
192
|
+
if (result.error) {
|
|
193
|
+
setError(result.error);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
if (!isEdit) {
|
|
197
|
+
localStorage.setItem("palmier:lastAgent", agent);
|
|
198
|
+
}
|
|
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);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
localStorage.setItem("palmier:lastAgent", agent);
|
|
227
|
+
onRun(result.task_id!, result.run_id);
|
|
228
|
+
onCancel();
|
|
229
|
+
} catch (err) {
|
|
230
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
231
|
+
} finally {
|
|
232
|
+
setSavingAction(null);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div className="task-form-overlay">
|
|
239
|
+
<div className="task-form">
|
|
240
|
+
{planDialogOpen ? (
|
|
241
|
+
<PlanDialog
|
|
242
|
+
body={body}
|
|
243
|
+
permissions={initial?.permissions}
|
|
244
|
+
/>
|
|
245
|
+
) : (<>
|
|
246
|
+
<div className="task-form-header">
|
|
247
|
+
<h2>{initial ? "Edit Task" : "New Task"}</h2>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
{error && <div className="form-error">{error}</div>}
|
|
251
|
+
|
|
252
|
+
<textarea
|
|
253
|
+
autoFocus={!initial}
|
|
254
|
+
className="form-textarea"
|
|
255
|
+
value={userPrompt}
|
|
256
|
+
onChange={(e) => setUserPrompt(e.target.value)}
|
|
257
|
+
placeholder={commandEnabled
|
|
258
|
+
? "If the input email contains an event, create a calendar entry for it."
|
|
259
|
+
: "Research today's top AI news and write a summary."}
|
|
260
|
+
rows={4}
|
|
261
|
+
disabled={saving}
|
|
262
|
+
/>
|
|
263
|
+
|
|
264
|
+
<div className="plan-actions">
|
|
265
|
+
{hasPlan && (
|
|
266
|
+
<button
|
|
267
|
+
className="btn btn-link"
|
|
268
|
+
onClick={() => setPlanDialogOpen(true)}
|
|
269
|
+
>
|
|
270
|
+
Execution Plan
|
|
271
|
+
</button>
|
|
272
|
+
)}
|
|
273
|
+
<div className="agent-picker-section-inline" style={{ marginLeft: "auto" }}>
|
|
274
|
+
<span className="agent-picker-label">Run with</span>
|
|
275
|
+
<select
|
|
276
|
+
className="form-select form-select-sm"
|
|
277
|
+
value={agent}
|
|
278
|
+
onChange={(e) => setAgent(e.target.value)}
|
|
279
|
+
>
|
|
280
|
+
{agents.map((a) => (
|
|
281
|
+
<option key={a.key} value={a.key}>{a.label}</option>
|
|
282
|
+
))}
|
|
283
|
+
</select>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
<div className="toggles-group">
|
|
288
|
+
<div className={`command-section${commandEnabled ? " command-section-active" : ""}`}>
|
|
289
|
+
<label className="toggle-label">
|
|
290
|
+
<input
|
|
291
|
+
type="checkbox"
|
|
292
|
+
checked={commandEnabled}
|
|
293
|
+
onChange={(e) => {
|
|
294
|
+
setCommandEnabled(e.target.checked);
|
|
295
|
+
if (!e.target.checked) setCommand("");
|
|
296
|
+
else setTimeout(() => commandInputRef.current?.focus(), 0);
|
|
297
|
+
}}
|
|
298
|
+
disabled={saving}
|
|
299
|
+
/>
|
|
300
|
+
Reactive
|
|
301
|
+
</label>
|
|
302
|
+
{commandEnabled && (
|
|
303
|
+
<>
|
|
304
|
+
<p className="command-help-text">
|
|
305
|
+
Runs a command and invokes the task for each line of output.
|
|
306
|
+
Use “the input” in your task description to reference each line.
|
|
307
|
+
</p>
|
|
308
|
+
<input
|
|
309
|
+
ref={commandInputRef}
|
|
310
|
+
className="form-input form-input-mono"
|
|
311
|
+
type="text"
|
|
312
|
+
value={command}
|
|
313
|
+
onChange={(e) => setCommand(e.target.value)}
|
|
314
|
+
placeholder="gws gmail +watch --project my-project"
|
|
315
|
+
disabled={saving}
|
|
316
|
+
/>
|
|
317
|
+
</>
|
|
318
|
+
)}
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
{hostPlatform === "win32" && (
|
|
322
|
+
<label className="toggle-label">
|
|
323
|
+
<input
|
|
324
|
+
type="checkbox"
|
|
325
|
+
checked={foregroundMode}
|
|
326
|
+
onChange={(e) => setForegroundMode(e.target.checked)}
|
|
327
|
+
disabled={saving}
|
|
328
|
+
/>
|
|
329
|
+
Run in the foreground (host must login to Windows)
|
|
330
|
+
</label>
|
|
331
|
+
)}
|
|
332
|
+
|
|
333
|
+
<label className="toggle-label">
|
|
334
|
+
<input
|
|
335
|
+
type="checkbox"
|
|
336
|
+
checked={yoloMode}
|
|
337
|
+
onChange={(e) => setYoloMode(e.target.checked)}
|
|
338
|
+
disabled={saving}
|
|
339
|
+
/>
|
|
340
|
+
Yolo mode
|
|
341
|
+
</label>
|
|
342
|
+
{yoloMode && (
|
|
343
|
+
<p className="command-help-text">
|
|
344
|
+
The agent will auto-approve all tool calls without asking for permission.
|
|
345
|
+
</p>
|
|
346
|
+
)}
|
|
347
|
+
|
|
348
|
+
<div className="triggers-section">
|
|
349
|
+
<label className="toggle-label">
|
|
350
|
+
<input
|
|
351
|
+
type="checkbox"
|
|
352
|
+
checked={triggersEnabled}
|
|
353
|
+
onChange={(e) => {
|
|
354
|
+
if (e.target.checked) {
|
|
355
|
+
setTriggersEnabled(true);
|
|
356
|
+
if (triggerRows.length === 0) addRow();
|
|
357
|
+
} else {
|
|
358
|
+
setTriggersEnabled(false);
|
|
359
|
+
setRequiresConfirmation(false);
|
|
360
|
+
setTriggerRows((prev) => {
|
|
361
|
+
const cleaned = prev.filter((r) => {
|
|
362
|
+
if (r.schedule === "once") return r.onceDate && new Date(`${r.onceDate}T${r.onceTime}`) > new Date();
|
|
363
|
+
return true;
|
|
364
|
+
});
|
|
365
|
+
// Remove single untouched default row so check/uncheck round-trips cleanly
|
|
366
|
+
if (cleaned.length === 1 && cleaned[0].schedule === "daily" && cleaned[0].time === "00:00") return [];
|
|
367
|
+
return cleaned;
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}}
|
|
371
|
+
disabled={saving}
|
|
372
|
+
/>
|
|
373
|
+
Add schedules
|
|
374
|
+
</label>
|
|
375
|
+
<div className={`triggers-section-body${triggersEnabled ? "" : " disabled"}`}>
|
|
376
|
+
{triggerRows.map((row, i) => (
|
|
377
|
+
<div key={i} className="trigger-row-card">
|
|
378
|
+
<div className="trigger-row-content">
|
|
379
|
+
<div className="trigger-row-top">
|
|
380
|
+
<select
|
|
381
|
+
className="form-select"
|
|
382
|
+
value={row.schedule}
|
|
383
|
+
disabled={!triggersEnabled}
|
|
384
|
+
onChange={(e) => updateRow(i, { schedule: e.target.value as Schedule })}
|
|
385
|
+
>
|
|
386
|
+
<option value="once">Specific Time</option>
|
|
387
|
+
<option value="hourly">Hourly</option>
|
|
388
|
+
<option value="daily">Daily</option>
|
|
389
|
+
<option value="weekly">Weekly</option>
|
|
390
|
+
<option value="monthly">Monthly</option>
|
|
391
|
+
</select>
|
|
392
|
+
{row.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
|
+
{row.schedule === "weekly" && (
|
|
402
|
+
<>
|
|
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
|
+
</>
|
|
421
|
+
)}
|
|
422
|
+
</div>
|
|
423
|
+
{row.schedule === "monthly" && (
|
|
424
|
+
<div className="trigger-details">
|
|
425
|
+
<select
|
|
426
|
+
className="form-select"
|
|
427
|
+
value={row.dayOfMonth}
|
|
428
|
+
disabled={!triggersEnabled}
|
|
429
|
+
onChange={(e) => updateRow(i, { dayOfMonth: e.target.value })}
|
|
430
|
+
>
|
|
431
|
+
{Array.from({ length: 28 }, (_, n) => n + 1).map((d) => (
|
|
432
|
+
<option key={d} value={String(d)}>Day {d}</option>
|
|
433
|
+
))}
|
|
434
|
+
</select>
|
|
435
|
+
<input
|
|
436
|
+
className="form-input"
|
|
437
|
+
type="time"
|
|
438
|
+
value={row.time}
|
|
439
|
+
disabled={!triggersEnabled}
|
|
440
|
+
onChange={(e) => updateRow(i, { time: e.target.value })}
|
|
441
|
+
/>
|
|
442
|
+
</div>
|
|
443
|
+
)}
|
|
444
|
+
{row.schedule === "once" && (
|
|
445
|
+
<div className="trigger-details">
|
|
446
|
+
<input
|
|
447
|
+
className="form-input"
|
|
448
|
+
type="date"
|
|
449
|
+
value={row.onceDate}
|
|
450
|
+
min={new Date().toISOString().split("T")[0]}
|
|
451
|
+
disabled={!triggersEnabled}
|
|
452
|
+
onChange={(e) => updateRow(i, { onceDate: e.target.value })}
|
|
453
|
+
/>
|
|
454
|
+
<input
|
|
455
|
+
className="form-input"
|
|
456
|
+
type="time"
|
|
457
|
+
value={row.onceTime}
|
|
458
|
+
min={row.onceDate === new Date().toISOString().split("T")[0]
|
|
459
|
+
? new Date().toTimeString().slice(0, 5)
|
|
460
|
+
: undefined}
|
|
461
|
+
disabled={!triggersEnabled}
|
|
462
|
+
onChange={(e) => updateRow(i, { onceTime: e.target.value })}
|
|
463
|
+
/>
|
|
464
|
+
</div>
|
|
465
|
+
)}
|
|
466
|
+
</div>
|
|
467
|
+
<button
|
|
468
|
+
className="trigger-remove-btn"
|
|
469
|
+
onClick={() => removeRow(i)}
|
|
470
|
+
disabled={!triggersEnabled}
|
|
471
|
+
title="Remove trigger"
|
|
472
|
+
>
|
|
473
|
+
×
|
|
474
|
+
</button>
|
|
475
|
+
</div>
|
|
476
|
+
))}
|
|
477
|
+
{triggerRows.length > 0 && (
|
|
478
|
+
<button className="trigger-add-btn" onClick={addRow} disabled={!triggersEnabled}>
|
|
479
|
+
+ Add Schedule
|
|
480
|
+
</button>
|
|
481
|
+
)}
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
{triggersEnabled && triggerRows.length > 0 && (
|
|
485
|
+
<label className="toggle-label">
|
|
486
|
+
<input
|
|
487
|
+
type="checkbox"
|
|
488
|
+
checked={requiresConfirmation}
|
|
489
|
+
onChange={(e) => setRequiresConfirmation(e.target.checked)}
|
|
490
|
+
disabled={saving}
|
|
491
|
+
/>
|
|
492
|
+
Confirm before each run
|
|
493
|
+
</label>
|
|
494
|
+
)}
|
|
495
|
+
</div>
|
|
496
|
+
|
|
497
|
+
{!yoloMode && (() => {
|
|
498
|
+
const selected = agents.find((a) => a.key === agent);
|
|
499
|
+
return selected?.supportsPermissions === false && (
|
|
500
|
+
<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>
|
|
501
|
+
);
|
|
502
|
+
})()}
|
|
503
|
+
|
|
504
|
+
<div className="form-actions">
|
|
505
|
+
{(() => {
|
|
506
|
+
const hasSchedule = triggerRows.length > 0;
|
|
507
|
+
const canRun = !!userPrompt.trim() && (!commandEnabled || !!command.trim());
|
|
508
|
+
if (!isEdit) {
|
|
509
|
+
if (hasSchedule) {
|
|
510
|
+
// New task with schedule: "Schedule" only
|
|
511
|
+
return (
|
|
512
|
+
<button
|
|
513
|
+
className="btn btn-primary"
|
|
514
|
+
onClick={() => confirmYolo() && handleSave()}
|
|
515
|
+
disabled={!canSave || saving}
|
|
516
|
+
>
|
|
517
|
+
{savingAction === "save" && <span className="btn-spinner" />}
|
|
518
|
+
Schedule
|
|
519
|
+
</button>
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
// New task, no schedule: "Run" (primary) + "Save"
|
|
523
|
+
return (<>
|
|
524
|
+
<button
|
|
525
|
+
className="btn btn-primary"
|
|
526
|
+
onClick={() => confirmYolo() && handleRunOneoff()}
|
|
527
|
+
disabled={!canRun || saving}
|
|
528
|
+
>
|
|
529
|
+
{savingAction === "run" && <span className="btn-spinner" />}
|
|
530
|
+
Run
|
|
531
|
+
</button>
|
|
532
|
+
<button
|
|
533
|
+
className="btn btn-secondary"
|
|
534
|
+
onClick={() => confirmYolo() && handleSave()}
|
|
535
|
+
disabled={!canSave || saving}
|
|
536
|
+
>
|
|
537
|
+
Save
|
|
538
|
+
</button>
|
|
539
|
+
</>);
|
|
540
|
+
}
|
|
541
|
+
if (isDirty) {
|
|
542
|
+
// Edit, changed: Save only
|
|
543
|
+
return (
|
|
544
|
+
<button
|
|
545
|
+
className="btn btn-primary"
|
|
546
|
+
onClick={() => confirmYolo() && handleSave()}
|
|
547
|
+
disabled={!canSave || saving}
|
|
548
|
+
>
|
|
549
|
+
{savingAction === "save" && <span className="btn-spinner" />}
|
|
550
|
+
Save
|
|
551
|
+
</button>
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
// Edit, unchanged: disabled Save
|
|
555
|
+
return (
|
|
556
|
+
<button
|
|
557
|
+
className="btn btn-primary"
|
|
558
|
+
disabled
|
|
559
|
+
>
|
|
560
|
+
Save
|
|
561
|
+
</button>
|
|
562
|
+
);
|
|
563
|
+
})()}
|
|
564
|
+
<button
|
|
565
|
+
className="btn btn-secondary"
|
|
566
|
+
onClick={() => {
|
|
567
|
+
if (isDirty && userPrompt.trim() && !confirm("You have unsaved changes. Discard?")) return;
|
|
568
|
+
onCancel();
|
|
569
|
+
}}
|
|
570
|
+
style={{ marginLeft: "auto" }}
|
|
571
|
+
>
|
|
572
|
+
Cancel
|
|
573
|
+
</button>
|
|
574
|
+
</div>
|
|
575
|
+
|
|
576
|
+
</>)}
|
|
577
|
+
</div>
|
|
578
|
+
</div>
|
|
579
|
+
);
|
|
580
|
+
}
|