palmier 0.9.6 → 0.9.7

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