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.
Files changed (255) hide show
  1. package/README.md +28 -13
  2. package/dist/agents/agent.d.ts +0 -1
  3. package/dist/agents/agent.js +0 -1
  4. package/dist/agents/aider.d.ts +0 -1
  5. package/dist/agents/aider.js +0 -1
  6. package/dist/agents/claude.d.ts +0 -1
  7. package/dist/agents/claude.js +0 -1
  8. package/dist/agents/cline.d.ts +0 -1
  9. package/dist/agents/cline.js +0 -1
  10. package/dist/agents/codex.d.ts +0 -1
  11. package/dist/agents/codex.js +0 -1
  12. package/dist/agents/copilot.d.ts +0 -1
  13. package/dist/agents/copilot.js +0 -1
  14. package/dist/agents/cursor.d.ts +0 -1
  15. package/dist/agents/cursor.js +0 -1
  16. package/dist/agents/deepagents.d.ts +0 -1
  17. package/dist/agents/deepagents.js +0 -1
  18. package/dist/agents/droid.d.ts +0 -1
  19. package/dist/agents/droid.js +0 -1
  20. package/dist/agents/gemini.d.ts +0 -1
  21. package/dist/agents/gemini.js +0 -1
  22. package/dist/agents/goose.d.ts +0 -1
  23. package/dist/agents/goose.js +0 -1
  24. package/dist/agents/hermes.d.ts +0 -1
  25. package/dist/agents/hermes.js +0 -1
  26. package/dist/agents/kimi.d.ts +0 -1
  27. package/dist/agents/kimi.js +0 -1
  28. package/dist/agents/kiro.d.ts +0 -1
  29. package/dist/agents/kiro.js +0 -1
  30. package/dist/agents/openclaw.d.ts +0 -1
  31. package/dist/agents/openclaw.js +0 -1
  32. package/dist/agents/opencode.d.ts +0 -1
  33. package/dist/agents/opencode.js +0 -1
  34. package/dist/agents/qoder.d.ts +0 -1
  35. package/dist/agents/qoder.js +0 -1
  36. package/dist/agents/qwen.d.ts +0 -1
  37. package/dist/agents/qwen.js +0 -1
  38. package/dist/agents/shared-prompt.d.ts +0 -1
  39. package/dist/agents/shared-prompt.js +0 -1
  40. package/dist/client-store.d.ts +0 -1
  41. package/dist/client-store.js +0 -1
  42. package/dist/commands/clients.d.ts +0 -1
  43. package/dist/commands/clients.js +0 -1
  44. package/dist/commands/info.d.ts +0 -1
  45. package/dist/commands/info.js +0 -1
  46. package/dist/commands/init.d.ts +0 -1
  47. package/dist/commands/init.js +1 -2
  48. package/dist/commands/pair.d.ts +0 -1
  49. package/dist/commands/pair.js +0 -1
  50. package/dist/commands/restart.d.ts +0 -1
  51. package/dist/commands/restart.js +0 -1
  52. package/dist/commands/run.d.ts +0 -1
  53. package/dist/commands/run.js +19 -3
  54. package/dist/commands/serve.d.ts +0 -1
  55. package/dist/commands/serve.js +0 -1
  56. package/dist/commands/uninstall.d.ts +0 -1
  57. package/dist/commands/uninstall.js +0 -1
  58. package/dist/config.d.ts +0 -1
  59. package/dist/config.js +0 -1
  60. package/dist/event-queues.d.ts +0 -1
  61. package/dist/event-queues.js +0 -1
  62. package/dist/events.d.ts +0 -1
  63. package/dist/events.js +0 -1
  64. package/dist/index.d.ts +0 -1
  65. package/dist/index.js +0 -1
  66. package/dist/linked-device.d.ts +0 -1
  67. package/dist/linked-device.js +0 -1
  68. package/dist/mcp-handler.d.ts +0 -1
  69. package/dist/mcp-handler.js +0 -1
  70. package/dist/mcp-tools.d.ts +0 -1
  71. package/dist/mcp-tools.js +0 -1
  72. package/dist/nats-client.d.ts +0 -1
  73. package/dist/nats-client.js +0 -1
  74. package/dist/network.d.ts +0 -1
  75. package/dist/network.js +0 -1
  76. package/dist/notification-store.d.ts +0 -1
  77. package/dist/notification-store.js +0 -1
  78. package/dist/pending-requests.d.ts +0 -1
  79. package/dist/pending-requests.js +0 -1
  80. package/dist/platform/index.d.ts +0 -1
  81. package/dist/platform/index.js +0 -1
  82. package/dist/platform/linux.d.ts +0 -1
  83. package/dist/platform/linux.js +0 -1
  84. package/dist/platform/macos.d.ts +0 -1
  85. package/dist/platform/macos.js +0 -1
  86. package/dist/platform/platform.d.ts +0 -1
  87. package/dist/platform/platform.js +0 -1
  88. package/dist/platform/windows.d.ts +0 -1
  89. package/dist/platform/windows.js +0 -1
  90. package/dist/pwa/assets/{index-MLEFUP3r.js → index-DWvRAUiy.js} +31 -31
  91. package/dist/pwa/assets/{web-B1sKCc7e.js → web-C4iZbqTC.js} +1 -1
  92. package/dist/pwa/assets/{web-ETD-8ZHd.js → web-CBFqJGX6.js} +1 -1
  93. package/dist/pwa/assets/{web-B4xEa6WO.js → web-DL4uXOpS.js} +1 -1
  94. package/dist/pwa/index.html +2 -2
  95. package/dist/rpc-handler.d.ts +0 -1
  96. package/dist/rpc-handler.js +0 -1
  97. package/dist/sms-store.d.ts +0 -1
  98. package/dist/sms-store.js +0 -1
  99. package/dist/spawn-command.d.ts +0 -1
  100. package/dist/spawn-command.js +0 -1
  101. package/dist/task.d.ts +0 -1
  102. package/dist/task.js +0 -1
  103. package/dist/transports/http-transport.d.ts +0 -1
  104. package/dist/transports/http-transport.js +0 -1
  105. package/dist/transports/nats-transport.d.ts +0 -1
  106. package/dist/transports/nats-transport.js +0 -1
  107. package/dist/types.d.ts +0 -1
  108. package/dist/types.js +0 -1
  109. package/dist/update-checker.d.ts +0 -1
  110. package/dist/update-checker.js +0 -1
  111. package/package.json +11 -1
  112. package/.github/workflows/ci.yml +0 -16
  113. package/.github/workflows/publish.yml +0 -37
  114. package/CLAUDE.md +0 -22
  115. package/dist/pwa/apple-touch-icon.png +0 -0
  116. package/dist/pwa/manifest.webmanifest +0 -1
  117. package/dist/pwa/pwa-192x192.png +0 -0
  118. package/dist/pwa/pwa-512x512.png +0 -0
  119. package/dist/pwa/registerSW.js +0 -1
  120. package/dist/pwa/service-worker.js +0 -2
  121. package/palmier-server/.github/workflows/ci.yml +0 -21
  122. package/palmier-server/.github/workflows/deploy.yml +0 -38
  123. package/palmier-server/CLAUDE.md +0 -17
  124. package/palmier-server/PRODUCTION.md +0 -358
  125. package/palmier-server/README.md +0 -231
  126. package/palmier-server/nats.conf +0 -19
  127. package/palmier-server/package.json +0 -15
  128. package/palmier-server/pnpm-lock.yaml +0 -7639
  129. package/palmier-server/pnpm-workspace.yaml +0 -3
  130. package/palmier-server/pwa/index.html +0 -16
  131. package/palmier-server/pwa/logo/logo_20260421.png +0 -0
  132. package/palmier-server/pwa/package.json +0 -34
  133. package/palmier-server/pwa/public/apple-touch-icon.png +0 -0
  134. package/palmier-server/pwa/public/favicon.ico +0 -0
  135. package/palmier-server/pwa/public/pwa-192x192.png +0 -0
  136. package/palmier-server/pwa/public/pwa-512x512.png +0 -0
  137. package/palmier-server/pwa/src/App.css +0 -3012
  138. package/palmier-server/pwa/src/App.tsx +0 -59
  139. package/palmier-server/pwa/src/agentLabels.ts +0 -11
  140. package/palmier-server/pwa/src/api.ts +0 -67
  141. package/palmier-server/pwa/src/components/CapabilityToggles.tsx +0 -170
  142. package/palmier-server/pwa/src/components/ConnectionStatusIcon.tsx +0 -113
  143. package/palmier-server/pwa/src/components/HostMenu.tsx +0 -429
  144. package/palmier-server/pwa/src/components/PermissionsDialog.tsx +0 -34
  145. package/palmier-server/pwa/src/components/PullToRefreshIndicator.tsx +0 -46
  146. package/palmier-server/pwa/src/components/RunDetailView.tsx +0 -343
  147. package/palmier-server/pwa/src/components/SessionComposer.tsx +0 -157
  148. package/palmier-server/pwa/src/components/SessionsView.tsx +0 -326
  149. package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +0 -170
  150. package/palmier-server/pwa/src/components/TabBar.tsx +0 -40
  151. package/palmier-server/pwa/src/components/TaskCard.tsx +0 -255
  152. package/palmier-server/pwa/src/components/TaskForm.tsx +0 -766
  153. package/palmier-server/pwa/src/components/TasksView.tsx +0 -179
  154. package/palmier-server/pwa/src/constants.ts +0 -2
  155. package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +0 -432
  156. package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +0 -124
  157. package/palmier-server/pwa/src/draftGuard.ts +0 -24
  158. package/palmier-server/pwa/src/formatTime.ts +0 -44
  159. package/palmier-server/pwa/src/hooks/useBackClose.ts +0 -75
  160. package/palmier-server/pwa/src/hooks/useMediaQuery.ts +0 -17
  161. package/palmier-server/pwa/src/hooks/usePullToRefresh.ts +0 -102
  162. package/palmier-server/pwa/src/hooks/usePushSubscription.ts +0 -77
  163. package/palmier-server/pwa/src/main.tsx +0 -14
  164. package/palmier-server/pwa/src/native/Device.ts +0 -49
  165. package/palmier-server/pwa/src/pages/Dashboard.tsx +0 -542
  166. package/palmier-server/pwa/src/pages/PairHost.tsx +0 -232
  167. package/palmier-server/pwa/src/pages/PairSetup.tsx +0 -134
  168. package/palmier-server/pwa/src/service-worker.ts +0 -142
  169. package/palmier-server/pwa/src/types.ts +0 -75
  170. package/palmier-server/pwa/src/vite-env.d.ts +0 -11
  171. package/palmier-server/pwa/tsconfig.json +0 -21
  172. package/palmier-server/pwa/tsconfig.node.json +0 -19
  173. package/palmier-server/pwa/vite.config.ts +0 -47
  174. package/palmier-server/server/.env.example +0 -20
  175. package/palmier-server/server/package.json +0 -36
  176. package/palmier-server/server/src/db.ts +0 -44
  177. package/palmier-server/server/src/fcm.ts +0 -74
  178. package/palmier-server/server/src/index.ts +0 -688
  179. package/palmier-server/server/src/nats-jwt.ts +0 -299
  180. package/palmier-server/server/src/nats-setup.ts +0 -48
  181. package/palmier-server/server/src/nats.ts +0 -33
  182. package/palmier-server/server/src/notify.ts +0 -34
  183. package/palmier-server/server/src/push.ts +0 -68
  184. package/palmier-server/server/src/routes/device.ts +0 -224
  185. package/palmier-server/server/src/routes/fcm.ts +0 -64
  186. package/palmier-server/server/src/routes/hosts.ts +0 -56
  187. package/palmier-server/server/src/routes/push.ts +0 -101
  188. package/palmier-server/server/tsconfig.json +0 -20
  189. package/palmier-server/spec.md +0 -533
  190. package/src/agents/agent-instructions.md +0 -28
  191. package/src/agents/agent.ts +0 -114
  192. package/src/agents/aider.ts +0 -35
  193. package/src/agents/claude.ts +0 -39
  194. package/src/agents/cline.ts +0 -35
  195. package/src/agents/codex.ts +0 -40
  196. package/src/agents/copilot.ts +0 -37
  197. package/src/agents/cursor.ts +0 -36
  198. package/src/agents/deepagents.ts +0 -36
  199. package/src/agents/droid.ts +0 -35
  200. package/src/agents/gemini.ts +0 -43
  201. package/src/agents/goose.ts +0 -33
  202. package/src/agents/hermes.ts +0 -36
  203. package/src/agents/kimi.ts +0 -35
  204. package/src/agents/kiro.ts +0 -36
  205. package/src/agents/openclaw.ts +0 -29
  206. package/src/agents/opencode.ts +0 -36
  207. package/src/agents/qoder.ts +0 -36
  208. package/src/agents/qwen.ts +0 -32
  209. package/src/agents/shared-prompt.ts +0 -30
  210. package/src/client-store.ts +0 -68
  211. package/src/commands/clients.ts +0 -29
  212. package/src/commands/info.ts +0 -29
  213. package/src/commands/init.ts +0 -165
  214. package/src/commands/pair.ts +0 -137
  215. package/src/commands/restart.ts +0 -6
  216. package/src/commands/run.ts +0 -608
  217. package/src/commands/serve.ts +0 -211
  218. package/src/commands/uninstall.ts +0 -9
  219. package/src/config.ts +0 -36
  220. package/src/cross-spawn.d.ts +0 -5
  221. package/src/event-queues.ts +0 -41
  222. package/src/events.ts +0 -29
  223. package/src/index.ts +0 -111
  224. package/src/linked-device.ts +0 -52
  225. package/src/mcp-handler.ts +0 -200
  226. package/src/mcp-tools.ts +0 -839
  227. package/src/nats-client.ts +0 -19
  228. package/src/network.ts +0 -96
  229. package/src/notification-store.ts +0 -30
  230. package/src/pending-requests.ts +0 -73
  231. package/src/platform/index.ts +0 -20
  232. package/src/platform/linux.ts +0 -296
  233. package/src/platform/macos.ts +0 -329
  234. package/src/platform/platform.ts +0 -31
  235. package/src/platform/windows.ts +0 -299
  236. package/src/rpc-handler.ts +0 -691
  237. package/src/sms-store.ts +0 -28
  238. package/src/spawn-command.ts +0 -123
  239. package/src/task.ts +0 -343
  240. package/src/transports/http-transport.ts +0 -478
  241. package/src/transports/nats-transport.ts +0 -76
  242. package/src/types.ts +0 -89
  243. package/src/update-checker.ts +0 -40
  244. package/test/agent-instructions.test.ts +0 -209
  245. package/test/agent-output-parsing.test.ts +0 -74
  246. package/test/linux-cron.test.ts +0 -41
  247. package/test/macos-plist.test.ts +0 -112
  248. package/test/notification-store.test.ts +0 -57
  249. package/test/pairing.test.ts +0 -35
  250. package/test/result-state.test.ts +0 -110
  251. package/test/task-parsing.test.ts +0 -82
  252. package/test/taskrun-messages.test.ts +0 -224
  253. package/test/tsconfig.json +0 -9
  254. package/test/windows-xml.test.ts +0 -89
  255. 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 &ldquo;the new {scheduleMode === "on_new_notification" ? "notification" : "SMS"}&rdquo;.
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 &ldquo;the input&rdquo; 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
- &times;
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
- }