palmier 0.9.5 → 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 (258) hide show
  1. package/README.md +30 -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 +3 -4
  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/apple-touch-icon.png +0 -0
  91. package/dist/pwa/assets/index-D1bIhEbd.css +1 -0
  92. package/dist/pwa/assets/{index-Cvffaohh.js → index-DWvRAUiy.js} +31 -31
  93. package/dist/pwa/assets/{web-qdLcAD7T.js → web-C4iZbqTC.js} +1 -1
  94. package/dist/pwa/assets/{web-ChtbM4nv.js → web-CBFqJGX6.js} +1 -1
  95. package/dist/pwa/assets/{web-hExASsqW.js → web-DL4uXOpS.js} +1 -1
  96. package/dist/pwa/favicon.ico +0 -0
  97. package/dist/pwa/index.html +3 -3
  98. package/dist/pwa/manifest.webmanifest +1 -1
  99. package/dist/pwa/pwa-192x192.png +0 -0
  100. package/dist/pwa/pwa-512x512.png +0 -0
  101. package/dist/pwa/service-worker.js +1 -1
  102. package/dist/rpc-handler.d.ts +0 -1
  103. package/dist/rpc-handler.js +3 -10
  104. package/dist/sms-store.d.ts +0 -1
  105. package/dist/sms-store.js +0 -1
  106. package/dist/spawn-command.d.ts +0 -1
  107. package/dist/spawn-command.js +0 -1
  108. package/dist/task.d.ts +1 -1
  109. package/dist/task.js +14 -1
  110. package/dist/transports/http-transport.d.ts +0 -1
  111. package/dist/transports/http-transport.js +0 -1
  112. package/dist/transports/nats-transport.d.ts +0 -1
  113. package/dist/transports/nats-transport.js +0 -1
  114. package/dist/types.d.ts +0 -1
  115. package/dist/types.js +0 -1
  116. package/dist/update-checker.d.ts +0 -1
  117. package/dist/update-checker.js +0 -1
  118. package/package.json +5 -1
  119. package/.github/workflows/ci.yml +0 -16
  120. package/.github/workflows/publish.yml +0 -37
  121. package/CLAUDE.md +0 -22
  122. package/dist/pwa/assets/index-DBgOYBrB.css +0 -1
  123. package/palmier-server/.github/workflows/ci.yml +0 -21
  124. package/palmier-server/.github/workflows/deploy.yml +0 -38
  125. package/palmier-server/CLAUDE.md +0 -17
  126. package/palmier-server/PRODUCTION.md +0 -358
  127. package/palmier-server/README.md +0 -231
  128. package/palmier-server/nats.conf +0 -19
  129. package/palmier-server/package.json +0 -15
  130. package/palmier-server/pnpm-lock.yaml +0 -7639
  131. package/palmier-server/pnpm-workspace.yaml +0 -3
  132. package/palmier-server/pwa/index.html +0 -16
  133. package/palmier-server/pwa/logo/logo-prompt.md +0 -28
  134. package/palmier-server/pwa/logo/logo_20260330.png +0 -0
  135. package/palmier-server/pwa/package.json +0 -34
  136. package/palmier-server/pwa/public/apple-touch-icon.png +0 -0
  137. package/palmier-server/pwa/public/favicon.ico +0 -0
  138. package/palmier-server/pwa/public/pwa-192x192.png +0 -0
  139. package/palmier-server/pwa/public/pwa-512x512.png +0 -0
  140. package/palmier-server/pwa/src/App.css +0 -3004
  141. package/palmier-server/pwa/src/App.tsx +0 -59
  142. package/palmier-server/pwa/src/agentLabels.ts +0 -11
  143. package/palmier-server/pwa/src/api.ts +0 -67
  144. package/palmier-server/pwa/src/components/CapabilityToggles.tsx +0 -170
  145. package/palmier-server/pwa/src/components/ConnectionStatusIcon.tsx +0 -114
  146. package/palmier-server/pwa/src/components/HostMenu.tsx +0 -429
  147. package/palmier-server/pwa/src/components/PermissionsDialog.tsx +0 -34
  148. package/palmier-server/pwa/src/components/PullToRefreshIndicator.tsx +0 -46
  149. package/palmier-server/pwa/src/components/RunDetailView.tsx +0 -343
  150. package/palmier-server/pwa/src/components/SessionComposer.tsx +0 -157
  151. package/palmier-server/pwa/src/components/SessionsView.tsx +0 -326
  152. package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +0 -170
  153. package/palmier-server/pwa/src/components/TabBar.tsx +0 -40
  154. package/palmier-server/pwa/src/components/TaskCard.tsx +0 -255
  155. package/palmier-server/pwa/src/components/TaskForm.tsx +0 -764
  156. package/palmier-server/pwa/src/components/TasksView.tsx +0 -179
  157. package/palmier-server/pwa/src/constants.ts +0 -2
  158. package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +0 -432
  159. package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +0 -124
  160. package/palmier-server/pwa/src/draftGuard.ts +0 -24
  161. package/palmier-server/pwa/src/formatTime.ts +0 -44
  162. package/palmier-server/pwa/src/hooks/useBackClose.ts +0 -75
  163. package/palmier-server/pwa/src/hooks/useMediaQuery.ts +0 -17
  164. package/palmier-server/pwa/src/hooks/usePullToRefresh.ts +0 -102
  165. package/palmier-server/pwa/src/hooks/usePushSubscription.ts +0 -77
  166. package/palmier-server/pwa/src/main.tsx +0 -14
  167. package/palmier-server/pwa/src/native/Device.ts +0 -49
  168. package/palmier-server/pwa/src/pages/Dashboard.tsx +0 -542
  169. package/palmier-server/pwa/src/pages/PairHost.tsx +0 -232
  170. package/palmier-server/pwa/src/pages/PairSetup.tsx +0 -134
  171. package/palmier-server/pwa/src/service-worker.ts +0 -142
  172. package/palmier-server/pwa/src/types.ts +0 -75
  173. package/palmier-server/pwa/src/vite-env.d.ts +0 -11
  174. package/palmier-server/pwa/tsconfig.json +0 -21
  175. package/palmier-server/pwa/tsconfig.node.json +0 -19
  176. package/palmier-server/pwa/vite.config.ts +0 -47
  177. package/palmier-server/server/.env.example +0 -20
  178. package/palmier-server/server/package.json +0 -36
  179. package/palmier-server/server/src/db.ts +0 -44
  180. package/palmier-server/server/src/fcm.ts +0 -74
  181. package/palmier-server/server/src/index.ts +0 -688
  182. package/palmier-server/server/src/nats-jwt.ts +0 -299
  183. package/palmier-server/server/src/nats-setup.ts +0 -48
  184. package/palmier-server/server/src/nats.ts +0 -33
  185. package/palmier-server/server/src/notify.ts +0 -34
  186. package/palmier-server/server/src/push.ts +0 -68
  187. package/palmier-server/server/src/routes/device.ts +0 -224
  188. package/palmier-server/server/src/routes/fcm.ts +0 -64
  189. package/palmier-server/server/src/routes/hosts.ts +0 -56
  190. package/palmier-server/server/src/routes/push.ts +0 -101
  191. package/palmier-server/server/tsconfig.json +0 -20
  192. package/palmier-server/spec.md +0 -533
  193. package/src/agents/agent-instructions.md +0 -28
  194. package/src/agents/agent.ts +0 -114
  195. package/src/agents/aider.ts +0 -35
  196. package/src/agents/claude.ts +0 -39
  197. package/src/agents/cline.ts +0 -35
  198. package/src/agents/codex.ts +0 -40
  199. package/src/agents/copilot.ts +0 -37
  200. package/src/agents/cursor.ts +0 -36
  201. package/src/agents/deepagents.ts +0 -36
  202. package/src/agents/droid.ts +0 -35
  203. package/src/agents/gemini.ts +0 -43
  204. package/src/agents/goose.ts +0 -33
  205. package/src/agents/hermes.ts +0 -36
  206. package/src/agents/kimi.ts +0 -35
  207. package/src/agents/kiro.ts +0 -36
  208. package/src/agents/openclaw.ts +0 -29
  209. package/src/agents/opencode.ts +0 -36
  210. package/src/agents/qoder.ts +0 -36
  211. package/src/agents/qwen.ts +0 -32
  212. package/src/agents/shared-prompt.ts +0 -30
  213. package/src/client-store.ts +0 -68
  214. package/src/commands/clients.ts +0 -29
  215. package/src/commands/info.ts +0 -29
  216. package/src/commands/init.ts +0 -165
  217. package/src/commands/pair.ts +0 -137
  218. package/src/commands/restart.ts +0 -6
  219. package/src/commands/run.ts +0 -608
  220. package/src/commands/serve.ts +0 -211
  221. package/src/commands/uninstall.ts +0 -9
  222. package/src/config.ts +0 -36
  223. package/src/cross-spawn.d.ts +0 -5
  224. package/src/event-queues.ts +0 -41
  225. package/src/events.ts +0 -29
  226. package/src/index.ts +0 -111
  227. package/src/linked-device.ts +0 -52
  228. package/src/mcp-handler.ts +0 -200
  229. package/src/mcp-tools.ts +0 -839
  230. package/src/nats-client.ts +0 -19
  231. package/src/network.ts +0 -96
  232. package/src/notification-store.ts +0 -30
  233. package/src/pending-requests.ts +0 -73
  234. package/src/platform/index.ts +0 -20
  235. package/src/platform/linux.ts +0 -296
  236. package/src/platform/macos.ts +0 -329
  237. package/src/platform/platform.ts +0 -31
  238. package/src/platform/windows.ts +0 -299
  239. package/src/rpc-handler.ts +0 -694
  240. package/src/sms-store.ts +0 -28
  241. package/src/spawn-command.ts +0 -123
  242. package/src/task.ts +0 -330
  243. package/src/transports/http-transport.ts +0 -478
  244. package/src/transports/nats-transport.ts +0 -76
  245. package/src/types.ts +0 -89
  246. package/src/update-checker.ts +0 -40
  247. package/test/agent-instructions.test.ts +0 -209
  248. package/test/agent-output-parsing.test.ts +0 -74
  249. package/test/linux-cron.test.ts +0 -41
  250. package/test/macos-plist.test.ts +0 -112
  251. package/test/notification-store.test.ts +0 -57
  252. package/test/pairing.test.ts +0 -35
  253. package/test/result-state.test.ts +0 -110
  254. package/test/task-parsing.test.ts +0 -82
  255. package/test/taskrun-messages.test.ts +0 -224
  256. package/test/tsconfig.json +0 -9
  257. package/test/windows-xml.test.ts +0 -89
  258. package/tsconfig.json +0 -19
@@ -1,764 +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">Schedule</h3>
435
- <select
436
- className="form-select"
437
- value={scheduleMode}
438
- onChange={(e) => changeScheduleMode(e.target.value as ScheduleMode)}
439
- disabled={saving}
440
- >
441
- <option value="ondemand">On Demand</option>
442
- <option value="specific_times">Specific Times</option>
443
- <option value="hourly">Hourly</option>
444
- <option value="daily">Daily</option>
445
- <option value="weekly">Weekly</option>
446
- <option value="monthly">Monthly</option>
447
- <option value="on_new_notification">On New Notification</option>
448
- <option value="on_new_sms">On New SMS</option>
449
- <option value="command">Command-triggered</option>
450
- </select>
451
-
452
- {modeIsEvent && (
453
- <div className="schedule-reactive">
454
- <p className="command-help-text">
455
- {scheduleMode === "on_new_notification"
456
- ? "Runs each time a new push notification arrives on the paired Android device."
457
- : "Runs each time a new SMS arrives on the paired Android device."}
458
- {" "}The triggering payload is spliced into your task prompt — reference it as &ldquo;the new {scheduleMode === "on_new_notification" ? "notification" : "SMS"}&rdquo;.
459
- </p>
460
- {scheduleMode === "on_new_notification" && (() => {
461
- const selected = knownApps.find((a) => a.packageName === notificationApp);
462
- const selectedLabel = selected?.appName || notificationApp;
463
- return (
464
- <>
465
- {isNotificationListener ? (
466
- notificationApp.trim() ? (
467
- <div className="app-filter-selected">
468
- <span className="app-filter-selected-name">{selectedLabel}</span>
469
- {selected?.appName && <span className="app-filter-selected-pkg">{selected.packageName}</span>}
470
- <button
471
- type="button"
472
- className="app-filter-selected-clear"
473
- onClick={() => setNotificationApp("")}
474
- aria-label="Clear app filter"
475
- disabled={saving}
476
- >
477
-
478
- </button>
479
- </div>
480
- ) : (
481
- <button
482
- type="button"
483
- className="btn btn-link app-filter-trigger"
484
- onClick={() => { setAppSearch(""); setAppFilterOpen(true); }}
485
- disabled={saving}
486
- >
487
- Select app
488
- </button>
489
- )
490
- ) : (
491
- <input
492
- className="form-input"
493
- type="text"
494
- value={notificationApp}
495
- onChange={(e) => setNotificationApp(e.target.value)}
496
- placeholder="App (optional), e.g. com.google.android.gm"
497
- disabled={saving}
498
- />
499
- )}
500
- <p className="command-help-text app-filter-help">
501
- {notificationApp.trim()
502
- ? `Only notifications from ${selectedLabel} will trigger this task.`
503
- : "Every notification from your device triggers this task."}
504
- </p>
505
- </>
506
- );
507
- })()}
508
- {scheduleMode === "on_new_sms" && (
509
- <>
510
- <input
511
- className="form-input"
512
- type="text"
513
- value={smsSender}
514
- onChange={(e) => setSmsSender(e.target.value)}
515
- placeholder="From (optional), e.g. +1 555-1234)"
516
- disabled={saving}
517
- />
518
- <p className="command-help-text app-filter-help">
519
- {smsSender.trim()
520
- ? `Only SMS from ${smsSender.trim()} will trigger this task. Formatting (spaces, dashes, parens) is ignored.`
521
- : "Every SMS that arrives on your device triggers this task."}
522
- </p>
523
- </>
524
- )}
525
- </div>
526
- )}
527
-
528
- {modeIsCommand && (
529
- <div className="schedule-reactive">
530
- <p className="command-help-text">
531
- Runs a command and invokes the task for each line of output.
532
- Use &ldquo;the input&rdquo; in your task description to reference each line.
533
- </p>
534
- <input
535
- ref={commandInputRef}
536
- className="form-input form-input-mono"
537
- type="text"
538
- value={command}
539
- onChange={(e) => setCommand(e.target.value)}
540
- placeholder="gws gmail +watch --project my-project"
541
- disabled={saving}
542
- />
543
- </div>
544
- )}
545
-
546
- {modeIsScheduled && scheduleMode !== "hourly" && (
547
- <>
548
- {triggerRows.map((row, i) => (
549
- <div key={i} className="trigger-row-card">
550
- <div className="trigger-row-content">
551
- {scheduleMode === "daily" && (
552
- <input
553
- className="form-input"
554
- type="time"
555
- value={row.time}
556
- onChange={(e) => updateRow(i, { time: e.target.value })}
557
- />
558
- )}
559
- {scheduleMode === "weekly" && (
560
- <div className="trigger-row-top">
561
- <select
562
- className="form-select"
563
- value={row.dayOfWeek}
564
- onChange={(e) => updateRow(i, { dayOfWeek: e.target.value })}
565
- >
566
- {DAYS_OF_WEEK.map((d, di) => (
567
- <option key={di} value={String(di)}>{d}</option>
568
- ))}
569
- </select>
570
- <input
571
- className="form-input"
572
- type="time"
573
- value={row.time}
574
- onChange={(e) => updateRow(i, { time: e.target.value })}
575
- />
576
- </div>
577
- )}
578
- {scheduleMode === "monthly" && (
579
- <div className="trigger-row-top">
580
- <select
581
- className="form-select"
582
- value={row.dayOfMonth}
583
- onChange={(e) => updateRow(i, { dayOfMonth: e.target.value })}
584
- >
585
- {Array.from({ length: 28 }, (_, n) => n + 1).map((d) => (
586
- <option key={d} value={String(d)}>Day {d}</option>
587
- ))}
588
- </select>
589
- <input
590
- className="form-input"
591
- type="time"
592
- value={row.time}
593
- onChange={(e) => updateRow(i, { time: e.target.value })}
594
- />
595
- </div>
596
- )}
597
- {scheduleMode === "specific_times" && (
598
- <div className="trigger-row-top">
599
- <input
600
- className="form-input"
601
- type="date"
602
- value={row.onceDate}
603
- min={hostNow.date}
604
- onChange={(e) => updateRow(i, { onceDate: e.target.value })}
605
- />
606
- <input
607
- className="form-input"
608
- type="time"
609
- value={row.onceTime}
610
- min={row.onceDate === hostNow.date ? hostNow.time : undefined}
611
- onChange={(e) => updateRow(i, { onceTime: e.target.value })}
612
- />
613
- </div>
614
- )}
615
- </div>
616
- {triggerRows.length > 1 && (
617
- <button
618
- className="trigger-remove-btn"
619
- onClick={() => removeRow(i)}
620
- title="Remove trigger"
621
- >
622
- &times;
623
- </button>
624
- )}
625
- </div>
626
- ))}
627
- <button className="trigger-add-btn" onClick={addRow}>
628
- + Add
629
- </button>
630
- </>
631
- )}
632
-
633
- {hostPlatform === "win32" && (
634
- <label className="toggle-label">
635
- <input
636
- type="checkbox"
637
- checked={foregroundMode}
638
- onChange={(e) => setForegroundMode(e.target.checked)}
639
- disabled={saving}
640
- />
641
- Run in the foreground (host must login to Windows)
642
- </label>
643
- )}
644
- {modeIsScheduled && (
645
- <label className="toggle-label">
646
- <input
647
- type="checkbox"
648
- checked={requiresConfirmation}
649
- onChange={(e) => setRequiresConfirmation(e.target.checked)}
650
- disabled={saving}
651
- />
652
- Confirm before each run
653
- </label>
654
- )}
655
- {scheduleMode !== "ondemand" && (
656
- <label className="toggle-label">
657
- <input
658
- type="checkbox"
659
- checked={scheduleEnabled}
660
- onChange={(e) => setScheduleEnabled(e.target.checked)}
661
- disabled={saving}
662
- />
663
- Enabled
664
- </label>
665
- )}
666
- </div>
667
- </div>
668
-
669
- {!yoloMode && selectedAgent && !selectedAgent.supportsPermissions && (
670
- <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>
671
- )}
672
-
673
- <div className="form-actions">
674
- <button
675
- className="btn btn-primary"
676
- onClick={() => confirmYolo() && handleSave()}
677
- disabled={!canSave || saving}
678
- >
679
- {saving && <span className="btn-spinner" />}
680
- {saveButtonLabel}
681
- </button>
682
- <button
683
- className="btn btn-secondary"
684
- onClick={() => {
685
- if (isDirty && userPrompt.trim() && !confirm("You have unsaved changes. Discard?")) return;
686
- onCancel();
687
- }}
688
- style={{ marginLeft: "auto" }}
689
- >
690
- Cancel
691
- </button>
692
- </div>
693
-
694
- </>)}
695
- </div>
696
- {appFilterOpen && (() => {
697
- const q = appSearch.trim().toLowerCase();
698
- const filtered = q
699
- ? knownApps.filter((a) => a.packageName.toLowerCase().includes(q) || a.appName.toLowerCase().includes(q))
700
- : knownApps;
701
- return (
702
- <div className="app-filter-overlay" onClick={closeAppFilter}>
703
- <div className="app-filter-dialog" onClick={(e) => e.stopPropagation()}>
704
- <div className="app-filter-header">
705
- <h2>Select app</h2>
706
- <button className="app-filter-close" onClick={closeAppFilter} aria-label="Close">
707
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
708
- <path d="M4 4L12 12M12 4L4 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
709
- </svg>
710
- </button>
711
- </div>
712
- <input
713
- className="form-input app-filter-search"
714
- type="text"
715
- value={appSearch}
716
- onChange={(e) => setAppSearch(e.target.value)}
717
- onKeyDown={(e) => {
718
- if (e.key === "Enter" && appSearch.trim()) {
719
- setNotificationApp(appSearch.trim());
720
- closeAppFilter();
721
- }
722
- }}
723
- placeholder="Search or type a package name"
724
- autoFocus
725
- />
726
- <ul className="app-filter-list">
727
- {knownAppsLoading && knownApps.length === 0
728
- ? Array.from({ length: 6 }).map((_, i) => (
729
- <li key={`sk-${i}`} className="app-filter-row app-filter-skeleton">
730
- <div className="app-filter-skeleton-bar" />
731
- </li>
732
- ))
733
- : filtered.length === 0 && !appSearch.trim()
734
- ? <li className="app-filter-empty">No apps</li>
735
- : filtered.map((a) => (
736
- <li
737
- key={a.packageName}
738
- className="app-filter-row"
739
- onClick={() => { setNotificationApp(a.packageName); closeAppFilter(); }}
740
- >
741
- <div className="app-filter-row-labels">
742
- <div className="app-filter-row-name">{a.appName || a.packageName}</div>
743
- {a.appName && <div className="app-filter-row-pkg">{a.packageName}</div>}
744
- </div>
745
- </li>
746
- ))}
747
- {appSearch.trim() && (
748
- <li
749
- className="app-filter-row"
750
- onClick={() => { setNotificationApp(appSearch.trim()); closeAppFilter(); }}
751
- >
752
- <div className="app-filter-row-labels">
753
- <div className="app-filter-row-name">{appSearch.trim()}</div>
754
- </div>
755
- </li>
756
- )}
757
- </ul>
758
- </div>
759
- </div>
760
- );
761
- })()}
762
- </div>
763
- );
764
- }