palmier 0.9.6 → 0.9.8
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 +28 -13
- package/dist/agents/agent.d.ts +0 -1
- package/dist/agents/agent.js +0 -1
- package/dist/agents/aider.d.ts +0 -1
- package/dist/agents/aider.js +0 -1
- package/dist/agents/claude.d.ts +0 -1
- package/dist/agents/claude.js +0 -1
- package/dist/agents/cline.d.ts +0 -1
- package/dist/agents/cline.js +0 -1
- package/dist/agents/codex.d.ts +0 -1
- package/dist/agents/codex.js +0 -1
- package/dist/agents/copilot.d.ts +0 -1
- package/dist/agents/copilot.js +0 -1
- package/dist/agents/cursor.d.ts +0 -1
- package/dist/agents/cursor.js +0 -1
- package/dist/agents/deepagents.d.ts +0 -1
- package/dist/agents/deepagents.js +0 -1
- package/dist/agents/droid.d.ts +0 -1
- package/dist/agents/droid.js +0 -1
- package/dist/agents/gemini.d.ts +0 -1
- package/dist/agents/gemini.js +0 -1
- package/dist/agents/goose.d.ts +0 -1
- package/dist/agents/goose.js +0 -1
- package/dist/agents/hermes.d.ts +0 -1
- package/dist/agents/hermes.js +0 -1
- package/dist/agents/kimi.d.ts +0 -1
- package/dist/agents/kimi.js +0 -1
- package/dist/agents/kiro.d.ts +0 -1
- package/dist/agents/kiro.js +0 -1
- package/dist/agents/openclaw.d.ts +0 -1
- package/dist/agents/openclaw.js +0 -1
- package/dist/agents/opencode.d.ts +0 -1
- package/dist/agents/opencode.js +0 -1
- package/dist/agents/qoder.d.ts +0 -1
- package/dist/agents/qoder.js +0 -1
- package/dist/agents/qwen.d.ts +0 -1
- package/dist/agents/qwen.js +0 -1
- package/dist/agents/shared-prompt.d.ts +0 -1
- package/dist/agents/shared-prompt.js +0 -1
- package/dist/client-store.d.ts +0 -1
- package/dist/client-store.js +0 -1
- package/dist/commands/clients.d.ts +0 -1
- package/dist/commands/clients.js +0 -1
- package/dist/commands/info.d.ts +0 -1
- package/dist/commands/info.js +0 -1
- package/dist/commands/init.d.ts +0 -1
- package/dist/commands/init.js +1 -2
- package/dist/commands/pair.d.ts +0 -1
- package/dist/commands/pair.js +0 -1
- package/dist/commands/restart.d.ts +0 -1
- package/dist/commands/restart.js +0 -1
- package/dist/commands/run.d.ts +0 -1
- package/dist/commands/run.js +19 -3
- package/dist/commands/serve.d.ts +0 -1
- package/dist/commands/serve.js +0 -1
- package/dist/commands/uninstall.d.ts +0 -1
- package/dist/commands/uninstall.js +0 -1
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -1
- package/dist/event-queues.d.ts +0 -1
- package/dist/event-queues.js +0 -1
- package/dist/events.d.ts +0 -1
- package/dist/events.js +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/linked-device.d.ts +0 -1
- package/dist/linked-device.js +0 -1
- package/dist/mcp-handler.d.ts +0 -1
- package/dist/mcp-handler.js +0 -1
- package/dist/mcp-tools.d.ts +0 -1
- package/dist/mcp-tools.js +0 -1
- package/dist/nats-client.d.ts +0 -1
- package/dist/nats-client.js +0 -1
- package/dist/network.d.ts +0 -1
- package/dist/network.js +0 -1
- package/dist/notification-store.d.ts +0 -1
- package/dist/notification-store.js +0 -1
- package/dist/pending-requests.d.ts +0 -1
- package/dist/pending-requests.js +0 -1
- package/dist/platform/index.d.ts +0 -1
- package/dist/platform/index.js +0 -1
- package/dist/platform/linux.d.ts +0 -1
- package/dist/platform/linux.js +0 -1
- package/dist/platform/macos.d.ts +0 -1
- package/dist/platform/macos.js +0 -1
- package/dist/platform/platform.d.ts +0 -1
- package/dist/platform/platform.js +0 -1
- package/dist/platform/windows.d.ts +0 -1
- package/dist/platform/windows.js +0 -1
- package/dist/pwa/assets/{index-MLEFUP3r.js → index-DWvRAUiy.js} +31 -31
- package/dist/pwa/assets/{web-B1sKCc7e.js → web-C4iZbqTC.js} +1 -1
- package/dist/pwa/assets/{web-ETD-8ZHd.js → web-CBFqJGX6.js} +1 -1
- package/dist/pwa/assets/{web-B4xEa6WO.js → web-DL4uXOpS.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/rpc-handler.d.ts +0 -1
- package/dist/rpc-handler.js +0 -1
- package/dist/sms-store.d.ts +0 -1
- package/dist/sms-store.js +0 -1
- package/dist/spawn-command.d.ts +0 -1
- package/dist/spawn-command.js +0 -1
- package/dist/task.d.ts +0 -1
- package/dist/task.js +0 -1
- package/dist/transports/http-transport.d.ts +0 -1
- package/dist/transports/http-transport.js +0 -1
- package/dist/transports/nats-transport.d.ts +0 -1
- package/dist/transports/nats-transport.js +0 -1
- package/dist/types.d.ts +0 -1
- package/dist/types.js +0 -1
- package/dist/update-checker.d.ts +0 -1
- package/dist/update-checker.js +0 -1
- package/package.json +11 -1
- package/.github/workflows/ci.yml +0 -16
- package/.github/workflows/publish.yml +0 -37
- package/CLAUDE.md +0 -22
- package/dist/pwa/apple-touch-icon.png +0 -0
- package/dist/pwa/manifest.webmanifest +0 -1
- package/dist/pwa/pwa-192x192.png +0 -0
- package/dist/pwa/pwa-512x512.png +0 -0
- package/dist/pwa/registerSW.js +0 -1
- package/dist/pwa/service-worker.js +0 -2
- package/palmier-server/.github/workflows/ci.yml +0 -21
- package/palmier-server/.github/workflows/deploy.yml +0 -38
- package/palmier-server/CLAUDE.md +0 -17
- package/palmier-server/PRODUCTION.md +0 -358
- package/palmier-server/README.md +0 -231
- package/palmier-server/nats.conf +0 -19
- package/palmier-server/package.json +0 -15
- package/palmier-server/pnpm-lock.yaml +0 -7639
- package/palmier-server/pnpm-workspace.yaml +0 -3
- package/palmier-server/pwa/index.html +0 -16
- package/palmier-server/pwa/logo/logo_20260421.png +0 -0
- package/palmier-server/pwa/package.json +0 -34
- 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 +0 -3012
- package/palmier-server/pwa/src/App.tsx +0 -59
- package/palmier-server/pwa/src/agentLabels.ts +0 -11
- package/palmier-server/pwa/src/api.ts +0 -67
- package/palmier-server/pwa/src/components/CapabilityToggles.tsx +0 -170
- package/palmier-server/pwa/src/components/ConnectionStatusIcon.tsx +0 -113
- package/palmier-server/pwa/src/components/HostMenu.tsx +0 -429
- package/palmier-server/pwa/src/components/PermissionsDialog.tsx +0 -34
- package/palmier-server/pwa/src/components/PullToRefreshIndicator.tsx +0 -46
- package/palmier-server/pwa/src/components/RunDetailView.tsx +0 -343
- package/palmier-server/pwa/src/components/SessionComposer.tsx +0 -157
- package/palmier-server/pwa/src/components/SessionsView.tsx +0 -326
- package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +0 -170
- package/palmier-server/pwa/src/components/TabBar.tsx +0 -40
- package/palmier-server/pwa/src/components/TaskCard.tsx +0 -255
- package/palmier-server/pwa/src/components/TaskForm.tsx +0 -766
- package/palmier-server/pwa/src/components/TasksView.tsx +0 -179
- package/palmier-server/pwa/src/constants.ts +0 -2
- package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +0 -432
- package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +0 -124
- package/palmier-server/pwa/src/draftGuard.ts +0 -24
- package/palmier-server/pwa/src/formatTime.ts +0 -44
- package/palmier-server/pwa/src/hooks/useBackClose.ts +0 -75
- package/palmier-server/pwa/src/hooks/useMediaQuery.ts +0 -17
- package/palmier-server/pwa/src/hooks/usePullToRefresh.ts +0 -102
- package/palmier-server/pwa/src/hooks/usePushSubscription.ts +0 -77
- package/palmier-server/pwa/src/main.tsx +0 -14
- package/palmier-server/pwa/src/native/Device.ts +0 -49
- package/palmier-server/pwa/src/pages/Dashboard.tsx +0 -542
- package/palmier-server/pwa/src/pages/PairHost.tsx +0 -232
- package/palmier-server/pwa/src/pages/PairSetup.tsx +0 -134
- package/palmier-server/pwa/src/service-worker.ts +0 -142
- package/palmier-server/pwa/src/types.ts +0 -75
- package/palmier-server/pwa/src/vite-env.d.ts +0 -11
- package/palmier-server/pwa/tsconfig.json +0 -21
- package/palmier-server/pwa/tsconfig.node.json +0 -19
- package/palmier-server/pwa/vite.config.ts +0 -47
- package/palmier-server/server/.env.example +0 -20
- package/palmier-server/server/package.json +0 -36
- package/palmier-server/server/src/db.ts +0 -44
- package/palmier-server/server/src/fcm.ts +0 -74
- package/palmier-server/server/src/index.ts +0 -688
- package/palmier-server/server/src/nats-jwt.ts +0 -299
- package/palmier-server/server/src/nats-setup.ts +0 -48
- package/palmier-server/server/src/nats.ts +0 -33
- package/palmier-server/server/src/notify.ts +0 -34
- package/palmier-server/server/src/push.ts +0 -68
- package/palmier-server/server/src/routes/device.ts +0 -224
- package/palmier-server/server/src/routes/fcm.ts +0 -64
- package/palmier-server/server/src/routes/hosts.ts +0 -56
- package/palmier-server/server/src/routes/push.ts +0 -101
- package/palmier-server/server/tsconfig.json +0 -20
- package/palmier-server/spec.md +0 -533
- package/src/agents/agent-instructions.md +0 -28
- package/src/agents/agent.ts +0 -114
- package/src/agents/aider.ts +0 -35
- package/src/agents/claude.ts +0 -39
- package/src/agents/cline.ts +0 -35
- package/src/agents/codex.ts +0 -40
- package/src/agents/copilot.ts +0 -37
- package/src/agents/cursor.ts +0 -36
- package/src/agents/deepagents.ts +0 -36
- package/src/agents/droid.ts +0 -35
- package/src/agents/gemini.ts +0 -43
- package/src/agents/goose.ts +0 -33
- package/src/agents/hermes.ts +0 -36
- package/src/agents/kimi.ts +0 -35
- package/src/agents/kiro.ts +0 -36
- package/src/agents/openclaw.ts +0 -29
- package/src/agents/opencode.ts +0 -36
- package/src/agents/qoder.ts +0 -36
- package/src/agents/qwen.ts +0 -32
- package/src/agents/shared-prompt.ts +0 -30
- package/src/client-store.ts +0 -68
- package/src/commands/clients.ts +0 -29
- package/src/commands/info.ts +0 -29
- package/src/commands/init.ts +0 -165
- package/src/commands/pair.ts +0 -137
- package/src/commands/restart.ts +0 -6
- package/src/commands/run.ts +0 -608
- package/src/commands/serve.ts +0 -211
- package/src/commands/uninstall.ts +0 -9
- package/src/config.ts +0 -36
- package/src/cross-spawn.d.ts +0 -5
- package/src/event-queues.ts +0 -41
- package/src/events.ts +0 -29
- package/src/index.ts +0 -111
- package/src/linked-device.ts +0 -52
- package/src/mcp-handler.ts +0 -200
- package/src/mcp-tools.ts +0 -839
- package/src/nats-client.ts +0 -19
- package/src/network.ts +0 -96
- package/src/notification-store.ts +0 -30
- package/src/pending-requests.ts +0 -73
- package/src/platform/index.ts +0 -20
- package/src/platform/linux.ts +0 -296
- package/src/platform/macos.ts +0 -329
- package/src/platform/platform.ts +0 -31
- package/src/platform/windows.ts +0 -299
- package/src/rpc-handler.ts +0 -691
- package/src/sms-store.ts +0 -28
- package/src/spawn-command.ts +0 -123
- package/src/task.ts +0 -343
- package/src/transports/http-transport.ts +0 -478
- package/src/transports/nats-transport.ts +0 -76
- package/src/types.ts +0 -89
- package/src/update-checker.ts +0 -40
- package/test/agent-instructions.test.ts +0 -209
- package/test/agent-output-parsing.test.ts +0 -74
- package/test/linux-cron.test.ts +0 -41
- package/test/macos-plist.test.ts +0 -112
- package/test/notification-store.test.ts +0 -57
- package/test/pairing.test.ts +0 -35
- package/test/result-state.test.ts +0 -110
- package/test/task-parsing.test.ts +0 -82
- package/test/taskrun-messages.test.ts +0 -224
- package/test/tsconfig.json +0 -9
- package/test/windows-xml.test.ts +0 -89
- package/tsconfig.json +0 -19
|
@@ -1,766 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState, useCallback, useRef } from "react";
|
|
2
|
-
import { Capacitor } from "@capacitor/core";
|
|
3
|
-
import { useHostConnection } from "../contexts/HostConnectionContext";
|
|
4
|
-
import { useHostStore } from "../contexts/HostStoreContext";
|
|
5
|
-
import { hostNowParts } from "../formatTime";
|
|
6
|
-
import PermissionsDialog from "./PermissionsDialog";
|
|
7
|
-
import { useBackClose } from "../hooks/useBackClose";
|
|
8
|
-
import { Device } from "../native/Device";
|
|
9
|
-
import type { AgentInfo, Task } from "../types";
|
|
10
|
-
|
|
11
|
-
type ScheduleType = "crons" | "specific_times" | "on_new_notification" | "on_new_sms";
|
|
12
|
-
type EventMode = "on_new_notification" | "on_new_sms";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const DAYS_OF_WEEK = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
16
|
-
|
|
17
|
-
type ScheduleSlot = "specific_times" | "hourly" | "daily" | "weekly" | "monthly";
|
|
18
|
-
type ScheduleMode = "ondemand" | "command" | EventMode | ScheduleSlot;
|
|
19
|
-
|
|
20
|
-
const SCHEDULE_SLOTS: readonly ScheduleMode[] = ["specific_times", "hourly", "daily", "weekly", "monthly"];
|
|
21
|
-
function isScheduleSlot(mode: ScheduleMode): mode is ScheduleSlot {
|
|
22
|
-
return (SCHEDULE_SLOTS as readonly string[]).includes(mode);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function isEventMode(mode: ScheduleMode): mode is EventMode {
|
|
26
|
-
return mode === "on_new_notification" || mode === "on_new_sms";
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface TriggerRow {
|
|
30
|
-
schedule: ScheduleSlot;
|
|
31
|
-
time: string;
|
|
32
|
-
dayOfWeek: string;
|
|
33
|
-
dayOfMonth: string;
|
|
34
|
-
onceDate: string;
|
|
35
|
-
onceTime: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function newRow(schedule: ScheduleSlot = "daily"): TriggerRow {
|
|
39
|
-
return { schedule, time: "00:00", dayOfWeek: "1", dayOfMonth: "1", onceDate: "", onceTime: "00:00" };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function cronToRow(cron: string): TriggerRow {
|
|
43
|
-
const parts = cron.split(" ");
|
|
44
|
-
if (parts.length !== 5) return newRow();
|
|
45
|
-
const [min, hour, dom, , dow] = parts;
|
|
46
|
-
if (hour === "*") return newRow("hourly");
|
|
47
|
-
const time = `${hour.padStart(2, "0")}:${min.padStart(2, "0")}`;
|
|
48
|
-
if (dow !== "*") return { ...newRow("weekly"), time, dayOfWeek: dow };
|
|
49
|
-
if (dom !== "*") return { ...newRow("monthly"), time, dayOfMonth: dom };
|
|
50
|
-
return { ...newRow("daily"), time };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function valueToRow(scheduleType: "crons" | "specific_times", value: string): TriggerRow {
|
|
54
|
-
if (scheduleType === "specific_times") {
|
|
55
|
-
const [datePart, timePart] = value.split("T");
|
|
56
|
-
return { ...newRow("specific_times"), onceDate: datePart ?? "", onceTime: (timePart ?? "09:00").slice(0, 5) };
|
|
57
|
-
}
|
|
58
|
-
return cronToRow(value);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function rowToCron(row: TriggerRow): string {
|
|
62
|
-
const [hh, mm] = row.time.split(":").map(Number);
|
|
63
|
-
switch (row.schedule) {
|
|
64
|
-
case "hourly": return "0 * * * *";
|
|
65
|
-
case "daily": return `${mm} ${hh} * * *`;
|
|
66
|
-
case "weekly": return `${mm} ${hh} * * ${row.dayOfWeek}`;
|
|
67
|
-
case "monthly": return `${mm} ${hh} ${row.dayOfMonth} * *`;
|
|
68
|
-
default: return "0 * * * *";
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function rowToValue(row: TriggerRow): string | null {
|
|
73
|
-
if (row.schedule === "specific_times") {
|
|
74
|
-
return row.onceDate ? `${row.onceDate}T${row.onceTime}` : null;
|
|
75
|
-
}
|
|
76
|
-
return rowToCron(row);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function modeToScheduleType(mode: ScheduleSlot | EventMode): ScheduleType {
|
|
80
|
-
if (mode === "specific_times") return "specific_times";
|
|
81
|
-
if (mode === "on_new_notification") return "on_new_notification";
|
|
82
|
-
if (mode === "on_new_sms") return "on_new_sms";
|
|
83
|
-
return "crons";
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function initialScheduleMode(initial?: Task): ScheduleMode {
|
|
87
|
-
if (!initial) return "daily";
|
|
88
|
-
if (initial.command) return "command";
|
|
89
|
-
const type = initial.schedule_type;
|
|
90
|
-
if (type === "on_new_notification" || type === "on_new_sms") return type;
|
|
91
|
-
if (type !== "crons" && type !== "specific_times") return "ondemand";
|
|
92
|
-
const first = initial.schedule_values?.[0];
|
|
93
|
-
if (!first) return "ondemand";
|
|
94
|
-
return valueToRow(type, first).schedule;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
interface TaskFormProps {
|
|
98
|
-
initial?: Task;
|
|
99
|
-
agents: AgentInfo[];
|
|
100
|
-
hostPlatform?: string;
|
|
101
|
-
isNotificationListener: boolean;
|
|
102
|
-
onSaved(task: Task): void;
|
|
103
|
-
onRun(taskId: string, runId?: string): void;
|
|
104
|
-
onCancel(): void;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export default function TaskForm({ initial, agents, hostPlatform, isNotificationListener, onSaved, onRun, onCancel }: TaskFormProps) {
|
|
108
|
-
const { request, activeHost } = useHostConnection();
|
|
109
|
-
const { setHostLastAgent } = useHostStore();
|
|
110
|
-
|
|
111
|
-
const defaultAgent = () => {
|
|
112
|
-
const lastAgent = activeHost.lastAgent;
|
|
113
|
-
const agentKeys = agents.map((a) => a.key);
|
|
114
|
-
if (lastAgent && agentKeys.includes(lastAgent)) return lastAgent;
|
|
115
|
-
return agents[0]?.key ?? "";
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const [userPrompt, setUserPrompt] = useState(initial?.user_prompt ?? "");
|
|
119
|
-
const [agent, setAgent] = useState(initial?.agent ?? defaultAgent());
|
|
120
|
-
|
|
121
|
-
const hasPermissions = !!initial?.permissions?.length;
|
|
122
|
-
|
|
123
|
-
const [planDialogOpen, setPlanDialogOpen] = useState(false);
|
|
124
|
-
const closePlanDialog = useCallback(() => setPlanDialogOpen(false), []);
|
|
125
|
-
useBackClose(planDialogOpen, closePlanDialog);
|
|
126
|
-
const [error, setError] = useState<string | null>(null);
|
|
127
|
-
|
|
128
|
-
const [scheduleMode, setScheduleMode] = useState<ScheduleMode>(() => initialScheduleMode(initial));
|
|
129
|
-
const [triggerRows, setTriggerRows] = useState<TriggerRow[]>(
|
|
130
|
-
() => {
|
|
131
|
-
const type = initial?.schedule_type;
|
|
132
|
-
const values = initial?.schedule_values;
|
|
133
|
-
if (values && (type === "crons" || type === "specific_times")) {
|
|
134
|
-
return values.map((v) => valueToRow(type, v));
|
|
135
|
-
}
|
|
136
|
-
const mode = initialScheduleMode(initial);
|
|
137
|
-
return isScheduleSlot(mode) ? [newRow(mode)] : [];
|
|
138
|
-
}
|
|
139
|
-
);
|
|
140
|
-
const [requiresConfirmation, setRequiresConfirmation] = useState(
|
|
141
|
-
initial?.requires_confirmation ?? false
|
|
142
|
-
);
|
|
143
|
-
const [scheduleEnabled, setScheduleEnabled] = useState(
|
|
144
|
-
initial?.schedule_type ? (initial.schedule_enabled ?? true) : true,
|
|
145
|
-
);
|
|
146
|
-
const [yoloMode, setYoloMode] = useState(initial?.yolo_mode ?? false);
|
|
147
|
-
const [foregroundMode, setForegroundMode] = useState(initial?.foreground_mode ?? false);
|
|
148
|
-
const [command, setCommand] = useState(initial?.command ?? "");
|
|
149
|
-
const [saving, setSaving] = useState(false);
|
|
150
|
-
|
|
151
|
-
// Single optional app filter for on_new_notification tasks. Empty = any app;
|
|
152
|
-
// non-empty = single packageName filter (stored as a one-entry schedule_values
|
|
153
|
-
// array on save). Datalist options come from the host-side app registry.
|
|
154
|
-
// No migration: pre-existing multi-app tasks truncate to the first entry on
|
|
155
|
-
// first edit.
|
|
156
|
-
const initialNotificationApp: string = (() => {
|
|
157
|
-
if (initial?.schedule_type === "on_new_notification" && initial.schedule_values && initial.schedule_values.length > 0) {
|
|
158
|
-
return initial.schedule_values[0];
|
|
159
|
-
}
|
|
160
|
-
return "";
|
|
161
|
-
})();
|
|
162
|
-
const [notificationApp, setNotificationApp] = useState<string>(initialNotificationApp);
|
|
163
|
-
const [knownApps, setKnownApps] = useState<Array<{ packageName: string; appName: string }>>([]);
|
|
164
|
-
const [knownAppsLoading, setKnownAppsLoading] = useState(false);
|
|
165
|
-
const [appFilterOpen, setAppFilterOpen] = useState(false);
|
|
166
|
-
const [appSearch, setAppSearch] = useState("");
|
|
167
|
-
const closeAppFilter = useCallback(() => setAppFilterOpen(false), []);
|
|
168
|
-
useBackClose(appFilterOpen, closeAppFilter);
|
|
169
|
-
|
|
170
|
-
// Only the notification-listening device can enumerate installed apps. On any
|
|
171
|
-
// other client we leave the list empty and fall back to a plain packageName
|
|
172
|
-
// input — the app registry we used to maintain on the host was inconsistent
|
|
173
|
-
// across devices, so we no longer cache it.
|
|
174
|
-
useEffect(() => {
|
|
175
|
-
if (scheduleMode !== "on_new_notification") return;
|
|
176
|
-
if (!isNotificationListener || !Capacitor.isNativePlatform() || !Device) return;
|
|
177
|
-
let cancelled = false;
|
|
178
|
-
setKnownAppsLoading(true);
|
|
179
|
-
Device.getInstalledApps()
|
|
180
|
-
.then(({ apps }) => {
|
|
181
|
-
if (cancelled) return;
|
|
182
|
-
const PALMIER_PACKAGE = "com.palmier.app";
|
|
183
|
-
const list = apps
|
|
184
|
-
.filter((a) => a.packageName !== PALMIER_PACKAGE)
|
|
185
|
-
.sort((a, b) => (a.appName || a.packageName).localeCompare(b.appName || b.packageName));
|
|
186
|
-
setKnownApps(list);
|
|
187
|
-
})
|
|
188
|
-
.catch(() => {})
|
|
189
|
-
.finally(() => { if (!cancelled) setKnownAppsLoading(false); });
|
|
190
|
-
return () => { cancelled = true; };
|
|
191
|
-
}, [scheduleMode, isNotificationListener]);
|
|
192
|
-
|
|
193
|
-
// Sender filter for on_new_sms tasks. Empty string = any sender; non-empty =
|
|
194
|
-
// whitelist a single sender (stored as a single-entry schedule_values array
|
|
195
|
-
// on save). Matching is normalized on the host — users don't need to worry
|
|
196
|
-
// about exact phone-number formatting.
|
|
197
|
-
const initialSmsSender: string = (() => {
|
|
198
|
-
if (initial?.schedule_type === "on_new_sms" && initial.schedule_values && initial.schedule_values.length > 0) {
|
|
199
|
-
return initial.schedule_values[0];
|
|
200
|
-
}
|
|
201
|
-
return "";
|
|
202
|
-
})();
|
|
203
|
-
const [smsSender, setSmsSender] = useState<string>(initialSmsSender);
|
|
204
|
-
|
|
205
|
-
const commandInputRef = useRef<HTMLInputElement>(null);
|
|
206
|
-
|
|
207
|
-
const modeIsScheduled = isScheduleSlot(scheduleMode);
|
|
208
|
-
const modeIsCommand = scheduleMode === "command";
|
|
209
|
-
const modeIsEvent = isEventMode(scheduleMode);
|
|
210
|
-
|
|
211
|
-
const selectedAgent = agents.find((a) => a.key === agent);
|
|
212
|
-
const agentSupportsYolo = !!selectedAgent?.supportsYolo;
|
|
213
|
-
|
|
214
|
-
// Force-disable yolo when the selected agent doesn't support it.
|
|
215
|
-
useEffect(() => {
|
|
216
|
-
if (!agentSupportsYolo && yoloMode) setYoloMode(false);
|
|
217
|
-
}, [agentSupportsYolo, yoloMode]);
|
|
218
|
-
|
|
219
|
-
const isEdit = !!initial;
|
|
220
|
-
const initialMode = initialScheduleMode(initial);
|
|
221
|
-
const isDirty = !isEdit
|
|
222
|
-
|| userPrompt !== (initial?.user_prompt ?? "")
|
|
223
|
-
|| agent !== (initial?.agent ?? "")
|
|
224
|
-
|| scheduleMode !== initialMode
|
|
225
|
-
|| requiresConfirmation !== (initial?.requires_confirmation ?? false)
|
|
226
|
-
|| scheduleEnabled !== (initial?.schedule_type ? (initial.schedule_enabled ?? true) : true)
|
|
227
|
-
|| yoloMode !== (initial?.yolo_mode ?? false)
|
|
228
|
-
|| foregroundMode !== (initial?.foreground_mode ?? false)
|
|
229
|
-
|| (modeIsCommand && command !== (initial?.command ?? ""))
|
|
230
|
-
|| (modeIsScheduled && (
|
|
231
|
-
JSON.stringify(collectScheduleValues()) !== JSON.stringify(initial?.schedule_values ?? [])
|
|
232
|
-
|| modeToScheduleType(scheduleMode as ScheduleSlot) !== (initial?.schedule_type ?? undefined)
|
|
233
|
-
))
|
|
234
|
-
|| (modeIsEvent && scheduleMode !== initial?.schedule_type)
|
|
235
|
-
|| (scheduleMode === "on_new_notification" && notificationApp.trim() !== initialNotificationApp.trim())
|
|
236
|
-
|| (scheduleMode === "on_new_sms" && smsSender.trim() !== initialSmsSender.trim());
|
|
237
|
-
|
|
238
|
-
const hostNow = hostNowParts(activeHost.timezone);
|
|
239
|
-
const hasInvalidTrigger = modeIsScheduled && triggerRows.some((r) =>
|
|
240
|
-
r.schedule === "specific_times" && (
|
|
241
|
-
!r.onceDate
|
|
242
|
-
|| r.onceDate < hostNow.date
|
|
243
|
-
|| (r.onceDate === hostNow.date && (r.onceTime ?? "") <= hostNow.time)
|
|
244
|
-
)
|
|
245
|
-
);
|
|
246
|
-
const canSave = isDirty
|
|
247
|
-
&& !!userPrompt.trim()
|
|
248
|
-
&& !hasInvalidTrigger
|
|
249
|
-
&& (!modeIsCommand || !!command.trim())
|
|
250
|
-
&& (!modeIsScheduled || triggerRows.length > 0);
|
|
251
|
-
|
|
252
|
-
function updateRow(index: number, patch: Partial<TriggerRow>) {
|
|
253
|
-
setTriggerRows((prev) => prev.map((r, i) => (i === index ? { ...r, ...patch } : r)));
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function removeRow(index: number) {
|
|
257
|
-
setTriggerRows((prev) => prev.filter((_, i) => i !== index));
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function addRow() {
|
|
261
|
-
if (!isScheduleSlot(scheduleMode)) return;
|
|
262
|
-
setTriggerRows((prev) => [...prev, prev.length > 0 ? { ...prev[prev.length - 1] } : newRow(scheduleMode)]);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function changeScheduleMode(next: ScheduleMode) {
|
|
266
|
-
setScheduleMode(next);
|
|
267
|
-
if (isScheduleSlot(next)) {
|
|
268
|
-
setTriggerRows([newRow(next)]);
|
|
269
|
-
if (next !== "specific_times" && next !== "hourly" && next !== "daily" && next !== "weekly" && next !== "monthly") {
|
|
270
|
-
// unreachable — appeases exhaustiveness
|
|
271
|
-
}
|
|
272
|
-
} else {
|
|
273
|
-
setTriggerRows([]);
|
|
274
|
-
setRequiresConfirmation(false);
|
|
275
|
-
}
|
|
276
|
-
if (next === "command") {
|
|
277
|
-
setTimeout(() => commandInputRef.current?.focus(), 0);
|
|
278
|
-
} else {
|
|
279
|
-
setCommand("");
|
|
280
|
-
}
|
|
281
|
-
if (next !== "on_new_notification") {
|
|
282
|
-
setNotificationApp(initialNotificationApp);
|
|
283
|
-
}
|
|
284
|
-
if (next !== "on_new_sms") {
|
|
285
|
-
setSmsSender(initialSmsSender);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function collectScheduleValues(): string[] {
|
|
290
|
-
return triggerRows.flatMap((row) => {
|
|
291
|
-
const v = rowToValue(row);
|
|
292
|
-
return v ? [v] : [];
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function confirmYolo(): boolean {
|
|
297
|
-
if (!yoloMode) return true;
|
|
298
|
-
return confirm(
|
|
299
|
-
"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?"
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
async function handleSave() {
|
|
304
|
-
setSaving(true);
|
|
305
|
-
setError(null);
|
|
306
|
-
try {
|
|
307
|
-
const scheduleValues = modeIsScheduled
|
|
308
|
-
? collectScheduleValues()
|
|
309
|
-
: scheduleMode === "on_new_notification" && notificationApp.trim()
|
|
310
|
-
? [notificationApp.trim()]
|
|
311
|
-
: scheduleMode === "on_new_sms" && smsSender.trim()
|
|
312
|
-
? [smsSender.trim()]
|
|
313
|
-
: [];
|
|
314
|
-
const scheduleType: ScheduleType | null = modeIsScheduled
|
|
315
|
-
? modeToScheduleType(scheduleMode as ScheduleSlot)
|
|
316
|
-
: modeIsEvent
|
|
317
|
-
? modeToScheduleType(scheduleMode as EventMode)
|
|
318
|
-
: null;
|
|
319
|
-
const payload: Record<string, unknown> = {
|
|
320
|
-
user_prompt: userPrompt,
|
|
321
|
-
agent,
|
|
322
|
-
schedule_type: scheduleType,
|
|
323
|
-
schedule_values: scheduleValues.length > 0 ? scheduleValues : null,
|
|
324
|
-
schedule_enabled: scheduleMode === "ondemand" ? true : scheduleEnabled,
|
|
325
|
-
requires_confirmation: modeIsScheduled ? requiresConfirmation : false,
|
|
326
|
-
yolo_mode: yoloMode,
|
|
327
|
-
foreground_mode: foregroundMode,
|
|
328
|
-
command: modeIsCommand ? command : "",
|
|
329
|
-
};
|
|
330
|
-
if (isEdit) {
|
|
331
|
-
payload.id = initial!.id;
|
|
332
|
-
}
|
|
333
|
-
const method = isEdit ? "task.update" : "task.create";
|
|
334
|
-
const result = await request<Task & { error?: string }>(method, payload, { timeout: 45000 });
|
|
335
|
-
if (result.error) {
|
|
336
|
-
setError(result.error);
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
if (!isEdit) setHostLastAgent(activeHost.hostId, agent);
|
|
340
|
-
|
|
341
|
-
// Command-triggered on create: save the task, then start it and navigate
|
|
342
|
-
// to the run. Event-triggered tasks are started by the daemon in response
|
|
343
|
-
// to the next NATS event, so we just save.
|
|
344
|
-
if (modeIsCommand && !isEdit) {
|
|
345
|
-
onSaved(result);
|
|
346
|
-
try {
|
|
347
|
-
const runResult = await request<{ run_id?: string; error?: string }>("task.run", { id: result.id });
|
|
348
|
-
if (runResult.run_id) onRun(result.id, runResult.run_id);
|
|
349
|
-
else if (!runResult.error) onRun(result.id);
|
|
350
|
-
} catch { /* task is saved; leave the user on the list if start fails */ }
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
onSaved(result);
|
|
355
|
-
} catch (err) {
|
|
356
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
357
|
-
} finally {
|
|
358
|
-
setSaving(false);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const saveButtonLabel = (() => {
|
|
363
|
-
if (isEdit) return "Save";
|
|
364
|
-
if (modeIsCommand) return "Run";
|
|
365
|
-
if ((modeIsScheduled || modeIsEvent) && scheduleEnabled) return "Schedule";
|
|
366
|
-
return "Save";
|
|
367
|
-
})();
|
|
368
|
-
|
|
369
|
-
return (
|
|
370
|
-
<div className="task-form-overlay">
|
|
371
|
-
<div className="task-form">
|
|
372
|
-
{planDialogOpen ? (
|
|
373
|
-
<PermissionsDialog permissions={initial?.permissions} />
|
|
374
|
-
) : (<>
|
|
375
|
-
<div className="task-form-header">
|
|
376
|
-
<h2>{initial ? "Edit Task" : "New Task"}</h2>
|
|
377
|
-
</div>
|
|
378
|
-
|
|
379
|
-
{error && <div className="form-error">{error}</div>}
|
|
380
|
-
|
|
381
|
-
<textarea
|
|
382
|
-
autoFocus={!initial}
|
|
383
|
-
className="form-textarea"
|
|
384
|
-
value={userPrompt}
|
|
385
|
-
onChange={(e) => setUserPrompt(e.target.value)}
|
|
386
|
-
placeholder={modeIsCommand
|
|
387
|
-
? "If the input email contains an event, create a calendar entry for it."
|
|
388
|
-
: "Research today's top AI news and write a summary."}
|
|
389
|
-
rows={4}
|
|
390
|
-
disabled={saving}
|
|
391
|
-
/>
|
|
392
|
-
|
|
393
|
-
<div className="plan-actions">
|
|
394
|
-
<div className="agent-picker-section-inline">
|
|
395
|
-
<span className="agent-picker-label">Run with</span>
|
|
396
|
-
<select
|
|
397
|
-
className="form-select form-select-sm"
|
|
398
|
-
value={agent}
|
|
399
|
-
onChange={(e) => setAgent(e.target.value)}
|
|
400
|
-
disabled={saving}
|
|
401
|
-
>
|
|
402
|
-
{agents.map((a) => (
|
|
403
|
-
<option key={a.key} value={a.key}>{a.label}</option>
|
|
404
|
-
))}
|
|
405
|
-
</select>
|
|
406
|
-
</div>
|
|
407
|
-
{agentSupportsYolo && (
|
|
408
|
-
<label className="yolo-inline" style={{ marginLeft: "auto" }}>
|
|
409
|
-
<input
|
|
410
|
-
type="checkbox"
|
|
411
|
-
checked={yoloMode}
|
|
412
|
-
onChange={(e) => setYoloMode(e.target.checked)}
|
|
413
|
-
disabled={saving}
|
|
414
|
-
/>
|
|
415
|
-
Yolo
|
|
416
|
-
</label>
|
|
417
|
-
)}
|
|
418
|
-
{agentSupportsYolo && yoloMode && (
|
|
419
|
-
<p className="yolo-warning">
|
|
420
|
-
The agent will auto-approve all tool calls without asking for permission.
|
|
421
|
-
</p>
|
|
422
|
-
)}
|
|
423
|
-
{hasPermissions && (
|
|
424
|
-
<div className="granted-permissions-row">
|
|
425
|
-
<button className="btn btn-link" onClick={() => setPlanDialogOpen(true)}>
|
|
426
|
-
Granted Permissions
|
|
427
|
-
</button>
|
|
428
|
-
</div>
|
|
429
|
-
)}
|
|
430
|
-
</div>
|
|
431
|
-
|
|
432
|
-
<div className="toggles-group">
|
|
433
|
-
<div className="schedule-section">
|
|
434
|
-
<h3 className="schedule-section-title">
|
|
435
|
-
Schedule <span className="schedule-section-hint">based on host time</span>
|
|
436
|
-
</h3>
|
|
437
|
-
<select
|
|
438
|
-
className="form-select"
|
|
439
|
-
value={scheduleMode}
|
|
440
|
-
onChange={(e) => changeScheduleMode(e.target.value as ScheduleMode)}
|
|
441
|
-
disabled={saving}
|
|
442
|
-
>
|
|
443
|
-
<option value="ondemand">On Demand</option>
|
|
444
|
-
<option value="specific_times">Specific Times</option>
|
|
445
|
-
<option value="hourly">Hourly</option>
|
|
446
|
-
<option value="daily">Daily</option>
|
|
447
|
-
<option value="weekly">Weekly</option>
|
|
448
|
-
<option value="monthly">Monthly</option>
|
|
449
|
-
<option value="on_new_notification">On New Notification</option>
|
|
450
|
-
<option value="on_new_sms">On New SMS</option>
|
|
451
|
-
<option value="command">Command-triggered</option>
|
|
452
|
-
</select>
|
|
453
|
-
|
|
454
|
-
{modeIsEvent && (
|
|
455
|
-
<div className="schedule-reactive">
|
|
456
|
-
<p className="command-help-text">
|
|
457
|
-
{scheduleMode === "on_new_notification"
|
|
458
|
-
? "Runs each time a new push notification arrives on the paired Android device."
|
|
459
|
-
: "Runs each time a new SMS arrives on the paired Android device."}
|
|
460
|
-
{" "}The triggering payload is spliced into your task prompt — reference it as “the new {scheduleMode === "on_new_notification" ? "notification" : "SMS"}”.
|
|
461
|
-
</p>
|
|
462
|
-
{scheduleMode === "on_new_notification" && (() => {
|
|
463
|
-
const selected = knownApps.find((a) => a.packageName === notificationApp);
|
|
464
|
-
const selectedLabel = selected?.appName || notificationApp;
|
|
465
|
-
return (
|
|
466
|
-
<>
|
|
467
|
-
{isNotificationListener ? (
|
|
468
|
-
notificationApp.trim() ? (
|
|
469
|
-
<div className="app-filter-selected">
|
|
470
|
-
<span className="app-filter-selected-name">{selectedLabel}</span>
|
|
471
|
-
{selected?.appName && <span className="app-filter-selected-pkg">{selected.packageName}</span>}
|
|
472
|
-
<button
|
|
473
|
-
type="button"
|
|
474
|
-
className="app-filter-selected-clear"
|
|
475
|
-
onClick={() => setNotificationApp("")}
|
|
476
|
-
aria-label="Clear app filter"
|
|
477
|
-
disabled={saving}
|
|
478
|
-
>
|
|
479
|
-
✕
|
|
480
|
-
</button>
|
|
481
|
-
</div>
|
|
482
|
-
) : (
|
|
483
|
-
<button
|
|
484
|
-
type="button"
|
|
485
|
-
className="btn btn-link app-filter-trigger"
|
|
486
|
-
onClick={() => { setAppSearch(""); setAppFilterOpen(true); }}
|
|
487
|
-
disabled={saving}
|
|
488
|
-
>
|
|
489
|
-
Select app
|
|
490
|
-
</button>
|
|
491
|
-
)
|
|
492
|
-
) : (
|
|
493
|
-
<input
|
|
494
|
-
className="form-input"
|
|
495
|
-
type="text"
|
|
496
|
-
value={notificationApp}
|
|
497
|
-
onChange={(e) => setNotificationApp(e.target.value)}
|
|
498
|
-
placeholder="App (optional), e.g. com.google.android.gm"
|
|
499
|
-
disabled={saving}
|
|
500
|
-
/>
|
|
501
|
-
)}
|
|
502
|
-
<p className="command-help-text app-filter-help">
|
|
503
|
-
{notificationApp.trim()
|
|
504
|
-
? `Only notifications from ${selectedLabel} will trigger this task.`
|
|
505
|
-
: "Every notification from your device triggers this task."}
|
|
506
|
-
</p>
|
|
507
|
-
</>
|
|
508
|
-
);
|
|
509
|
-
})()}
|
|
510
|
-
{scheduleMode === "on_new_sms" && (
|
|
511
|
-
<>
|
|
512
|
-
<input
|
|
513
|
-
className="form-input"
|
|
514
|
-
type="text"
|
|
515
|
-
value={smsSender}
|
|
516
|
-
onChange={(e) => setSmsSender(e.target.value)}
|
|
517
|
-
placeholder="From (optional), e.g. +1 555-1234)"
|
|
518
|
-
disabled={saving}
|
|
519
|
-
/>
|
|
520
|
-
<p className="command-help-text app-filter-help">
|
|
521
|
-
{smsSender.trim()
|
|
522
|
-
? `Only SMS from ${smsSender.trim()} will trigger this task. Formatting (spaces, dashes, parens) is ignored.`
|
|
523
|
-
: "Every SMS that arrives on your device triggers this task."}
|
|
524
|
-
</p>
|
|
525
|
-
</>
|
|
526
|
-
)}
|
|
527
|
-
</div>
|
|
528
|
-
)}
|
|
529
|
-
|
|
530
|
-
{modeIsCommand && (
|
|
531
|
-
<div className="schedule-reactive">
|
|
532
|
-
<p className="command-help-text">
|
|
533
|
-
Runs a command and invokes the task for each line of output.
|
|
534
|
-
Use “the input” in your task description to reference each line.
|
|
535
|
-
</p>
|
|
536
|
-
<input
|
|
537
|
-
ref={commandInputRef}
|
|
538
|
-
className="form-input form-input-mono"
|
|
539
|
-
type="text"
|
|
540
|
-
value={command}
|
|
541
|
-
onChange={(e) => setCommand(e.target.value)}
|
|
542
|
-
placeholder="gws gmail +watch --project my-project"
|
|
543
|
-
disabled={saving}
|
|
544
|
-
/>
|
|
545
|
-
</div>
|
|
546
|
-
)}
|
|
547
|
-
|
|
548
|
-
{modeIsScheduled && scheduleMode !== "hourly" && (
|
|
549
|
-
<>
|
|
550
|
-
{triggerRows.map((row, i) => (
|
|
551
|
-
<div key={i} className="trigger-row-card">
|
|
552
|
-
<div className="trigger-row-content">
|
|
553
|
-
{scheduleMode === "daily" && (
|
|
554
|
-
<input
|
|
555
|
-
className="form-input"
|
|
556
|
-
type="time"
|
|
557
|
-
value={row.time}
|
|
558
|
-
onChange={(e) => updateRow(i, { time: e.target.value })}
|
|
559
|
-
/>
|
|
560
|
-
)}
|
|
561
|
-
{scheduleMode === "weekly" && (
|
|
562
|
-
<div className="trigger-row-top">
|
|
563
|
-
<select
|
|
564
|
-
className="form-select"
|
|
565
|
-
value={row.dayOfWeek}
|
|
566
|
-
onChange={(e) => updateRow(i, { dayOfWeek: e.target.value })}
|
|
567
|
-
>
|
|
568
|
-
{DAYS_OF_WEEK.map((d, di) => (
|
|
569
|
-
<option key={di} value={String(di)}>{d}</option>
|
|
570
|
-
))}
|
|
571
|
-
</select>
|
|
572
|
-
<input
|
|
573
|
-
className="form-input"
|
|
574
|
-
type="time"
|
|
575
|
-
value={row.time}
|
|
576
|
-
onChange={(e) => updateRow(i, { time: e.target.value })}
|
|
577
|
-
/>
|
|
578
|
-
</div>
|
|
579
|
-
)}
|
|
580
|
-
{scheduleMode === "monthly" && (
|
|
581
|
-
<div className="trigger-row-top">
|
|
582
|
-
<select
|
|
583
|
-
className="form-select"
|
|
584
|
-
value={row.dayOfMonth}
|
|
585
|
-
onChange={(e) => updateRow(i, { dayOfMonth: e.target.value })}
|
|
586
|
-
>
|
|
587
|
-
{Array.from({ length: 28 }, (_, n) => n + 1).map((d) => (
|
|
588
|
-
<option key={d} value={String(d)}>Day {d}</option>
|
|
589
|
-
))}
|
|
590
|
-
</select>
|
|
591
|
-
<input
|
|
592
|
-
className="form-input"
|
|
593
|
-
type="time"
|
|
594
|
-
value={row.time}
|
|
595
|
-
onChange={(e) => updateRow(i, { time: e.target.value })}
|
|
596
|
-
/>
|
|
597
|
-
</div>
|
|
598
|
-
)}
|
|
599
|
-
{scheduleMode === "specific_times" && (
|
|
600
|
-
<div className="trigger-row-top">
|
|
601
|
-
<input
|
|
602
|
-
className="form-input"
|
|
603
|
-
type="date"
|
|
604
|
-
value={row.onceDate}
|
|
605
|
-
min={hostNow.date}
|
|
606
|
-
onChange={(e) => updateRow(i, { onceDate: e.target.value })}
|
|
607
|
-
/>
|
|
608
|
-
<input
|
|
609
|
-
className="form-input"
|
|
610
|
-
type="time"
|
|
611
|
-
value={row.onceTime}
|
|
612
|
-
min={row.onceDate === hostNow.date ? hostNow.time : undefined}
|
|
613
|
-
onChange={(e) => updateRow(i, { onceTime: e.target.value })}
|
|
614
|
-
/>
|
|
615
|
-
</div>
|
|
616
|
-
)}
|
|
617
|
-
</div>
|
|
618
|
-
{triggerRows.length > 1 && (
|
|
619
|
-
<button
|
|
620
|
-
className="trigger-remove-btn"
|
|
621
|
-
onClick={() => removeRow(i)}
|
|
622
|
-
title="Remove trigger"
|
|
623
|
-
>
|
|
624
|
-
×
|
|
625
|
-
</button>
|
|
626
|
-
)}
|
|
627
|
-
</div>
|
|
628
|
-
))}
|
|
629
|
-
<button className="trigger-add-btn" onClick={addRow}>
|
|
630
|
-
+ Add
|
|
631
|
-
</button>
|
|
632
|
-
</>
|
|
633
|
-
)}
|
|
634
|
-
|
|
635
|
-
{hostPlatform === "win32" && (
|
|
636
|
-
<label className="toggle-label">
|
|
637
|
-
<input
|
|
638
|
-
type="checkbox"
|
|
639
|
-
checked={foregroundMode}
|
|
640
|
-
onChange={(e) => setForegroundMode(e.target.checked)}
|
|
641
|
-
disabled={saving}
|
|
642
|
-
/>
|
|
643
|
-
Run in the foreground (host must login to Windows)
|
|
644
|
-
</label>
|
|
645
|
-
)}
|
|
646
|
-
{modeIsScheduled && (
|
|
647
|
-
<label className="toggle-label">
|
|
648
|
-
<input
|
|
649
|
-
type="checkbox"
|
|
650
|
-
checked={requiresConfirmation}
|
|
651
|
-
onChange={(e) => setRequiresConfirmation(e.target.checked)}
|
|
652
|
-
disabled={saving}
|
|
653
|
-
/>
|
|
654
|
-
Confirm before each run
|
|
655
|
-
</label>
|
|
656
|
-
)}
|
|
657
|
-
{scheduleMode !== "ondemand" && (
|
|
658
|
-
<label className="toggle-label">
|
|
659
|
-
<input
|
|
660
|
-
type="checkbox"
|
|
661
|
-
checked={scheduleEnabled}
|
|
662
|
-
onChange={(e) => setScheduleEnabled(e.target.checked)}
|
|
663
|
-
disabled={saving}
|
|
664
|
-
/>
|
|
665
|
-
Enabled
|
|
666
|
-
</label>
|
|
667
|
-
)}
|
|
668
|
-
</div>
|
|
669
|
-
</div>
|
|
670
|
-
|
|
671
|
-
{!yoloMode && selectedAgent && !selectedAgent.supportsPermissions && (
|
|
672
|
-
<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>
|
|
673
|
-
)}
|
|
674
|
-
|
|
675
|
-
<div className="form-actions">
|
|
676
|
-
<button
|
|
677
|
-
className="btn btn-primary"
|
|
678
|
-
onClick={() => confirmYolo() && handleSave()}
|
|
679
|
-
disabled={!canSave || saving}
|
|
680
|
-
>
|
|
681
|
-
{saving && <span className="btn-spinner" />}
|
|
682
|
-
{saveButtonLabel}
|
|
683
|
-
</button>
|
|
684
|
-
<button
|
|
685
|
-
className="btn btn-secondary"
|
|
686
|
-
onClick={() => {
|
|
687
|
-
if (isDirty && userPrompt.trim() && !confirm("You have unsaved changes. Discard?")) return;
|
|
688
|
-
onCancel();
|
|
689
|
-
}}
|
|
690
|
-
style={{ marginLeft: "auto" }}
|
|
691
|
-
>
|
|
692
|
-
Cancel
|
|
693
|
-
</button>
|
|
694
|
-
</div>
|
|
695
|
-
|
|
696
|
-
</>)}
|
|
697
|
-
</div>
|
|
698
|
-
{appFilterOpen && (() => {
|
|
699
|
-
const q = appSearch.trim().toLowerCase();
|
|
700
|
-
const filtered = q
|
|
701
|
-
? knownApps.filter((a) => a.packageName.toLowerCase().includes(q) || a.appName.toLowerCase().includes(q))
|
|
702
|
-
: knownApps;
|
|
703
|
-
return (
|
|
704
|
-
<div className="app-filter-overlay" onClick={closeAppFilter}>
|
|
705
|
-
<div className="app-filter-dialog" onClick={(e) => e.stopPropagation()}>
|
|
706
|
-
<div className="app-filter-header">
|
|
707
|
-
<h2>Select app</h2>
|
|
708
|
-
<button className="app-filter-close" onClick={closeAppFilter} aria-label="Close">
|
|
709
|
-
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
710
|
-
<path d="M4 4L12 12M12 4L4 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
711
|
-
</svg>
|
|
712
|
-
</button>
|
|
713
|
-
</div>
|
|
714
|
-
<input
|
|
715
|
-
className="form-input app-filter-search"
|
|
716
|
-
type="text"
|
|
717
|
-
value={appSearch}
|
|
718
|
-
onChange={(e) => setAppSearch(e.target.value)}
|
|
719
|
-
onKeyDown={(e) => {
|
|
720
|
-
if (e.key === "Enter" && appSearch.trim()) {
|
|
721
|
-
setNotificationApp(appSearch.trim());
|
|
722
|
-
closeAppFilter();
|
|
723
|
-
}
|
|
724
|
-
}}
|
|
725
|
-
placeholder="Search or type a package name"
|
|
726
|
-
autoFocus
|
|
727
|
-
/>
|
|
728
|
-
<ul className="app-filter-list">
|
|
729
|
-
{knownAppsLoading && knownApps.length === 0
|
|
730
|
-
? Array.from({ length: 6 }).map((_, i) => (
|
|
731
|
-
<li key={`sk-${i}`} className="app-filter-row app-filter-skeleton">
|
|
732
|
-
<div className="app-filter-skeleton-bar" />
|
|
733
|
-
</li>
|
|
734
|
-
))
|
|
735
|
-
: filtered.length === 0 && !appSearch.trim()
|
|
736
|
-
? <li className="app-filter-empty">No apps</li>
|
|
737
|
-
: filtered.map((a) => (
|
|
738
|
-
<li
|
|
739
|
-
key={a.packageName}
|
|
740
|
-
className="app-filter-row"
|
|
741
|
-
onClick={() => { setNotificationApp(a.packageName); closeAppFilter(); }}
|
|
742
|
-
>
|
|
743
|
-
<div className="app-filter-row-labels">
|
|
744
|
-
<div className="app-filter-row-name">{a.appName || a.packageName}</div>
|
|
745
|
-
{a.appName && <div className="app-filter-row-pkg">{a.packageName}</div>}
|
|
746
|
-
</div>
|
|
747
|
-
</li>
|
|
748
|
-
))}
|
|
749
|
-
{appSearch.trim() && (
|
|
750
|
-
<li
|
|
751
|
-
className="app-filter-row"
|
|
752
|
-
onClick={() => { setNotificationApp(appSearch.trim()); closeAppFilter(); }}
|
|
753
|
-
>
|
|
754
|
-
<div className="app-filter-row-labels">
|
|
755
|
-
<div className="app-filter-row-name">{appSearch.trim()}</div>
|
|
756
|
-
</div>
|
|
757
|
-
</li>
|
|
758
|
-
)}
|
|
759
|
-
</ul>
|
|
760
|
-
</div>
|
|
761
|
-
</div>
|
|
762
|
-
);
|
|
763
|
-
})()}
|
|
764
|
-
</div>
|
|
765
|
-
);
|
|
766
|
-
}
|