palmier 0.8.0 → 0.8.3

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 (132) hide show
  1. package/CLAUDE.md +13 -0
  2. package/README.md +11 -11
  3. package/dist/agents/agent.d.ts +0 -4
  4. package/dist/agents/claude.js +1 -1
  5. package/dist/agents/codex.js +2 -2
  6. package/dist/agents/cursor.js +1 -1
  7. package/dist/agents/deepagents.js +1 -1
  8. package/dist/agents/gemini.js +3 -2
  9. package/dist/agents/goose.js +1 -1
  10. package/dist/agents/hermes.js +1 -1
  11. package/dist/agents/kiro.js +1 -1
  12. package/dist/agents/opencode.js +1 -1
  13. package/dist/agents/qoder.js +1 -1
  14. package/dist/agents/shared-prompt.d.ts +0 -3
  15. package/dist/agents/shared-prompt.js +0 -3
  16. package/dist/app-registry.d.ts +10 -0
  17. package/dist/app-registry.js +44 -0
  18. package/dist/commands/info.d.ts +0 -3
  19. package/dist/commands/info.js +0 -5
  20. package/dist/commands/init.d.ts +0 -3
  21. package/dist/commands/init.js +2 -11
  22. package/dist/commands/pair.d.ts +1 -4
  23. package/dist/commands/pair.js +1 -12
  24. package/dist/commands/restart.d.ts +0 -3
  25. package/dist/commands/restart.js +0 -3
  26. package/dist/commands/run.d.ts +1 -14
  27. package/dist/commands/run.js +18 -61
  28. package/dist/commands/serve.d.ts +0 -3
  29. package/dist/commands/serve.js +33 -27
  30. package/dist/config.d.ts +0 -8
  31. package/dist/config.js +0 -8
  32. package/dist/device-capabilities.d.ts +1 -1
  33. package/dist/event-queues.d.ts +6 -21
  34. package/dist/event-queues.js +6 -21
  35. package/dist/events.d.ts +0 -6
  36. package/dist/events.js +1 -9
  37. package/dist/index.js +0 -1
  38. package/dist/mcp-handler.js +1 -2
  39. package/dist/mcp-tools.d.ts +0 -3
  40. package/dist/mcp-tools.js +14 -18
  41. package/dist/nats-client.d.ts +0 -3
  42. package/dist/nats-client.js +1 -4
  43. package/dist/pending-requests.d.ts +4 -18
  44. package/dist/pending-requests.js +4 -18
  45. package/dist/platform/index.d.ts +1 -4
  46. package/dist/platform/index.js +1 -4
  47. package/dist/platform/linux.d.ts +3 -9
  48. package/dist/platform/linux.js +9 -20
  49. package/dist/platform/platform.d.ts +1 -4
  50. package/dist/platform/windows.d.ts +2 -5
  51. package/dist/platform/windows.js +19 -39
  52. package/dist/pwa/assets/index-B0F9mtid.css +1 -0
  53. package/dist/pwa/assets/index-SYs3mcdJ.js +120 -0
  54. package/dist/pwa/assets/{web-CF-N8Di6.js → web-C6lkQj9J.js} +1 -1
  55. package/dist/pwa/assets/{web-BpM3fNCn.js → web-Z1623me-.js} +1 -1
  56. package/dist/pwa/index.html +2 -2
  57. package/dist/pwa/service-worker.js +1 -1
  58. package/dist/rpc-handler.d.ts +0 -6
  59. package/dist/rpc-handler.js +19 -48
  60. package/dist/spawn-command.d.ts +10 -25
  61. package/dist/spawn-command.js +7 -15
  62. package/dist/task.d.ts +6 -64
  63. package/dist/task.js +7 -70
  64. package/dist/transports/http-transport.d.ts +0 -4
  65. package/dist/transports/http-transport.js +6 -28
  66. package/dist/transports/nats-transport.d.ts +0 -4
  67. package/dist/transports/nats-transport.js +3 -9
  68. package/dist/types.d.ts +3 -7
  69. package/dist/update-checker.d.ts +1 -4
  70. package/dist/update-checker.js +2 -5
  71. package/package.json +1 -1
  72. package/palmier-server/README.md +1 -1
  73. package/palmier-server/pwa/src/App.css +170 -20
  74. package/palmier-server/pwa/src/App.tsx +15 -1
  75. package/palmier-server/pwa/src/components/HostMenu.tsx +282 -473
  76. package/palmier-server/pwa/src/components/RunDetailView.tsx +3 -3
  77. package/palmier-server/pwa/src/components/SessionsView.tsx +57 -25
  78. package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +160 -0
  79. package/palmier-server/pwa/src/components/TaskCard.tsx +12 -4
  80. package/palmier-server/pwa/src/components/TaskForm.tsx +230 -33
  81. package/palmier-server/pwa/src/components/TasksView.tsx +5 -0
  82. package/palmier-server/pwa/src/constants.ts +1 -1
  83. package/palmier-server/pwa/src/native/Device.ts +66 -0
  84. package/palmier-server/pwa/src/pages/Dashboard.tsx +11 -6
  85. package/palmier-server/pwa/src/pages/PairHost.tsx +18 -2
  86. package/palmier-server/pwa/src/types.ts +1 -1
  87. package/palmier-server/server/src/index.ts +7 -7
  88. package/palmier-server/server/src/routes/device.ts +4 -4
  89. package/palmier-server/spec.md +47 -6
  90. package/src/agents/agent.ts +0 -4
  91. package/src/agents/claude.ts +1 -1
  92. package/src/agents/codex.ts +2 -2
  93. package/src/agents/cursor.ts +1 -1
  94. package/src/agents/deepagents.ts +1 -1
  95. package/src/agents/gemini.ts +3 -2
  96. package/src/agents/goose.ts +1 -1
  97. package/src/agents/hermes.ts +1 -1
  98. package/src/agents/kiro.ts +1 -1
  99. package/src/agents/opencode.ts +1 -1
  100. package/src/agents/qoder.ts +1 -1
  101. package/src/agents/shared-prompt.ts +0 -3
  102. package/src/app-registry.ts +52 -0
  103. package/src/commands/info.ts +0 -5
  104. package/src/commands/init.ts +2 -11
  105. package/src/commands/pair.ts +1 -12
  106. package/src/commands/restart.ts +0 -3
  107. package/src/commands/run.ts +18 -65
  108. package/src/commands/serve.ts +31 -27
  109. package/src/config.ts +0 -8
  110. package/src/device-capabilities.ts +4 -3
  111. package/src/event-queues.ts +6 -21
  112. package/src/events.ts +1 -9
  113. package/src/index.ts +0 -1
  114. package/src/mcp-handler.ts +1 -2
  115. package/src/mcp-tools.ts +14 -20
  116. package/src/nats-client.ts +1 -4
  117. package/src/pending-requests.ts +4 -18
  118. package/src/platform/index.ts +1 -4
  119. package/src/platform/linux.ts +9 -20
  120. package/src/platform/platform.ts +1 -4
  121. package/src/platform/windows.ts +19 -40
  122. package/src/rpc-handler.ts +20 -48
  123. package/src/spawn-command.ts +11 -27
  124. package/src/task.ts +7 -70
  125. package/src/transports/http-transport.ts +6 -39
  126. package/src/transports/nats-transport.ts +3 -9
  127. package/src/types.ts +3 -10
  128. package/src/update-checker.ts +2 -5
  129. package/test/task-parsing.test.ts +2 -3
  130. package/test/windows-xml.test.ts +11 -12
  131. package/dist/pwa/assets/index-FP1Mipr6.js +0 -120
  132. package/dist/pwa/assets/index-bLTn8zBj.css +0 -1
@@ -131,7 +131,8 @@ export default function RunDetailView({ connected, hostId, request, subscribeEve
131
131
  }, [messages]);
132
132
 
133
133
  // On first load of a run, scroll the window to the bottom so the follow-up
134
- // input is visible, and focus the input if it's rendered (agent not running).
134
+ // input is visible. Deliberately not focusing the input on mobile that
135
+ // would pop the soft keyboard as soon as the run opens.
135
136
  useEffect(() => {
136
137
  if (loading || isLatestEmpty || !resolvedRunId) return;
137
138
  if (initialFocusForRunId.current === resolvedRunId) return;
@@ -139,9 +140,8 @@ export default function RunDetailView({ connected, hostId, request, subscribeEve
139
140
  requestAnimationFrame(() => {
140
141
  if (threadRef.current) threadRef.current.scrollTop = threadRef.current.scrollHeight;
141
142
  window.scrollTo({ top: document.documentElement.scrollHeight, behavior: "auto" });
142
- if (!isAgentGenerating) followupInputRef.current?.focus();
143
143
  });
144
- }, [loading, isLatestEmpty, resolvedRunId, isAgentGenerating]);
144
+ }, [loading, isLatestEmpty, resolvedRunId]);
145
145
 
146
146
  function typeLabel(type?: string): string | undefined {
147
147
  if (type === "input") return "User Input";
@@ -4,6 +4,7 @@ import { formatTime } from "../formatTime";
4
4
  import { confirmLeaveDraft } from "../draftGuard";
5
5
  import SessionComposer from "./SessionComposer";
6
6
  import PullToRefreshIndicator from "./PullToRefreshIndicator";
7
+ import SwipeToDeleteRow from "./SwipeToDeleteRow";
7
8
  import { usePullToRefresh } from "../hooks/usePullToRefresh";
8
9
  import type { AgentInfo, HistoryEntry } from "../types";
9
10
 
@@ -24,8 +25,25 @@ export default function SessionsView({ connected, hostId, request, subscribeEven
24
25
  const [total, setTotal] = useState(0);
25
26
  const [loading, setLoading] = useState(false);
26
27
  const [loadingMore, setLoadingMore] = useState(false);
28
+ /** Key of the row currently showing its delete action, or null. iOS pattern — at most one at a time. */
29
+ const [revealedKey, setRevealedKey] = useState<string | null>(null);
27
30
  const navigate = useNavigate();
28
31
 
32
+ async function deleteEntry(entry: HistoryEntry) {
33
+ const key = `${entry.task_id}:${entry.run_id}`;
34
+ // Optimistic: drop from the list immediately, restore if the RPC fails.
35
+ setEntries((prev) => prev.filter((e) => `${e.task_id}:${e.run_id}` !== key));
36
+ setTotal((t) => Math.max(0, t - 1));
37
+ setRevealedKey(null);
38
+ try {
39
+ await request("taskrun.delete", { task_id: entry.task_id, run_id: entry.run_id });
40
+ } catch (err) {
41
+ console.error("Failed to delete run:", err);
42
+ setEntries((prev) => [entry, ...prev]);
43
+ setTotal((t) => t + 1);
44
+ }
45
+ }
46
+
29
47
  const sentinelRef = useRef<HTMLDivElement>(null);
30
48
 
31
49
  // Build RPC params with optional task_id filter
@@ -247,32 +265,46 @@ export default function SessionsView({ connected, hostId, request, subscribeEven
247
265
  {composer}
248
266
  {filterChip}
249
267
  <div className="task-list">
250
- {entries.map((entry, i) => (
251
- <div
252
- key={`${entry.task_id}-${entry.run_id}-${i}`}
253
- className="sessions-card"
254
- onClick={() => !entry.error && handleCardClick(entry.task_id, entry.run_id)}
255
- >
256
- <div className="sessions-card-body">
257
- <h3 className="sessions-card-name">{entry.task_name || entry.task_id}</h3>
258
- <div className="sessions-card-meta">
259
- <span style={{ color: stateColor(entry.running_state) }}>
260
- {stateLabel[entry.running_state ?? ""] ?? entry.running_state}
261
- </span>
262
- {entry.end_time && <span>{formatTime(entry.end_time)}</span>}
263
- {entry.start_time && entry.end_time && (
264
- <span style={{ color: "var(--color-muted)" }}>
265
- {formatDuration(entry.start_time, entry.end_time)}
266
- </span>
267
- )}
268
- {entry.error && (
269
- <span style={{ color: "var(--color-muted)" }}>{entry.error}</span>
270
- )}
268
+ {entries.map((entry, i) => {
269
+ const key = `${entry.task_id}:${entry.run_id}`;
270
+ return (
271
+ <SwipeToDeleteRow
272
+ key={`${key}-${i}`}
273
+ id={key}
274
+ revealedId={revealedKey}
275
+ setRevealedId={setRevealedKey}
276
+ onDelete={() => deleteEntry(entry)}
277
+ onClick={() => !entry.error && handleCardClick(entry.task_id, entry.run_id)}
278
+ >
279
+ <div className="sessions-card">
280
+ <div className="sessions-card-body">
281
+ <h3 className="sessions-card-name">{entry.task_name || entry.task_id}</h3>
282
+ <div className="sessions-card-meta">
283
+ {entry.running_state === "started" ? (
284
+ <span className="status-spinner" aria-label="Running">
285
+ <span />
286
+ </span>
287
+ ) : (
288
+ <span style={{ color: stateColor(entry.running_state) }}>
289
+ {stateLabel[entry.running_state ?? ""] ?? entry.running_state}
290
+ </span>
291
+ )}
292
+ {entry.end_time && <span>{formatTime(entry.end_time)}</span>}
293
+ {entry.start_time && entry.end_time && (
294
+ <span style={{ color: "var(--color-muted)" }}>
295
+ {formatDuration(entry.start_time, entry.end_time)}
296
+ </span>
297
+ )}
298
+ {entry.error && (
299
+ <span style={{ color: "var(--color-muted)" }}>{entry.error}</span>
300
+ )}
301
+ </div>
302
+ </div>
303
+ <span className="sessions-card-chevron">&#8250;</span>
271
304
  </div>
272
- </div>
273
- <span className="sessions-card-chevron">&#8250;</span>
274
- </div>
275
- ))}
305
+ </SwipeToDeleteRow>
306
+ );
307
+ })}
276
308
 
277
309
  {/* Sentinel for infinite scroll */}
278
310
  <div ref={sentinelRef} style={{ height: 1 }} />
@@ -0,0 +1,160 @@
1
+ import { useEffect, useRef, useState, type ReactNode, type PointerEvent } from "react";
2
+
3
+ interface SwipeToDeleteRowProps {
4
+ /** Unique id used to coordinate "at most one row revealed" with the parent. */
5
+ id: string;
6
+ /** The id of the currently-revealed row (or null). Set to this row's id to reveal it. */
7
+ revealedId: string | null;
8
+ setRevealedId(id: string | null): void;
9
+ onDelete(): void;
10
+ onClick?(): void;
11
+ children: ReactNode;
12
+ /** Label for the action button (default "Delete"). */
13
+ actionLabel?: string;
14
+ }
15
+
16
+ const REVEAL_WIDTH = 88; // px width of the action button
17
+ const OPEN_THRESHOLD = REVEAL_WIDTH / 2;
18
+ const AXIS_LOCK_THRESHOLD = 6; // px of horizontal travel before we claim the gesture
19
+
20
+ /**
21
+ * Wraps a row with swipe-left to reveal a destructive action button.
22
+ * Tap the button to confirm, tap elsewhere to dismiss the reveal.
23
+ *
24
+ * Uses pointer events so the same code works for touch and mouse. A short
25
+ * axis-lock period at the start of a drag decides whether the user is
26
+ * scrolling vertically (let it through) or swiping horizontally (capture).
27
+ */
28
+ export default function SwipeToDeleteRow({
29
+ id,
30
+ revealedId,
31
+ setRevealedId,
32
+ onDelete,
33
+ onClick,
34
+ children,
35
+ actionLabel = "Delete",
36
+ }: SwipeToDeleteRowProps) {
37
+ const revealed = revealedId === id;
38
+ const [dragOffset, setDragOffset] = useState(0);
39
+ const [dragging, setDragging] = useState(false);
40
+
41
+ const startX = useRef(0);
42
+ const startY = useRef(0);
43
+ const axis = useRef<"x" | "y" | null>(null);
44
+ const baseOffset = useRef(0); // translateX when the gesture started
45
+ const movedEnough = useRef(false); // whether we should suppress the click that follows
46
+ const rowRef = useRef<HTMLDivElement>(null);
47
+
48
+ // Reset local drag offset whenever parent closes this row.
49
+ useEffect(() => {
50
+ if (!revealed) setDragOffset(0);
51
+ }, [revealed]);
52
+
53
+ // Close when the user taps elsewhere in the document.
54
+ useEffect(() => {
55
+ if (!revealed) return;
56
+ function onDocPointerDown(e: Event) {
57
+ if (rowRef.current?.contains(e.target as Node)) return;
58
+ setRevealedId(null);
59
+ }
60
+ document.addEventListener("pointerdown", onDocPointerDown);
61
+ return () => document.removeEventListener("pointerdown", onDocPointerDown);
62
+ }, [revealed, setRevealedId]);
63
+
64
+ function handlePointerDown(e: PointerEvent<HTMLDivElement>) {
65
+ // Ignore non-primary buttons (right-click etc.) so we don't steal them.
66
+ if (e.button !== undefined && e.button !== 0) return;
67
+ startX.current = e.clientX;
68
+ startY.current = e.clientY;
69
+ axis.current = null;
70
+ baseOffset.current = revealed ? -REVEAL_WIDTH : 0;
71
+ movedEnough.current = false;
72
+ }
73
+
74
+ function handlePointerMove(e: PointerEvent<HTMLDivElement>) {
75
+ if (e.pointerType === "mouse" && e.buttons === 0) return; // not dragging
76
+ const dx = e.clientX - startX.current;
77
+ const dy = e.clientY - startY.current;
78
+
79
+ if (axis.current === null) {
80
+ if (Math.abs(dx) < AXIS_LOCK_THRESHOLD && Math.abs(dy) < AXIS_LOCK_THRESHOLD) return;
81
+ axis.current = Math.abs(dx) > Math.abs(dy) ? "x" : "y";
82
+ if (axis.current === "x") {
83
+ try { (e.currentTarget as Element).setPointerCapture(e.pointerId); } catch { /* unsupported */ }
84
+ }
85
+ }
86
+ if (axis.current !== "x") return;
87
+
88
+ movedEnough.current = true;
89
+ if (!dragging) setDragging(true);
90
+ // Clamp: can swipe left to reveal fully, a bit of rubber-band on the right.
91
+ let next = baseOffset.current + dx;
92
+ if (next > 0) next = next / 4;
93
+ if (next < -REVEAL_WIDTH) next = -REVEAL_WIDTH + (next + REVEAL_WIDTH) / 4;
94
+ setDragOffset(next);
95
+ }
96
+
97
+ function handlePointerUp() {
98
+ if (axis.current !== "x") {
99
+ setDragging(false);
100
+ return;
101
+ }
102
+ axis.current = null;
103
+ setDragging(false);
104
+
105
+ const finalOffset = dragOffset;
106
+ const openNow = finalOffset <= -OPEN_THRESHOLD;
107
+ if (openNow) {
108
+ setDragOffset(-REVEAL_WIDTH);
109
+ setRevealedId(id);
110
+ } else {
111
+ setDragOffset(0);
112
+ if (revealed) setRevealedId(null);
113
+ }
114
+ }
115
+
116
+ function handleClickCapture(e: React.MouseEvent) {
117
+ // If the gesture translated the row, treat it as a swipe — not a click.
118
+ // Also absorb the click that re-hides a revealed row.
119
+ if (movedEnough.current) {
120
+ movedEnough.current = false;
121
+ e.stopPropagation();
122
+ e.preventDefault();
123
+ return;
124
+ }
125
+ if (revealed) {
126
+ e.stopPropagation();
127
+ e.preventDefault();
128
+ setRevealedId(null);
129
+ }
130
+ }
131
+
132
+ const currentOffset = dragOffset !== 0 ? dragOffset : (revealed ? -REVEAL_WIDTH : 0);
133
+
134
+ return (
135
+ <div ref={rowRef} className="swipe-row">
136
+ <button
137
+ type="button"
138
+ className="swipe-row-action"
139
+ style={{ width: REVEAL_WIDTH }}
140
+ onClick={(e) => { e.stopPropagation(); onDelete(); }}
141
+ tabIndex={revealed ? 0 : -1}
142
+ aria-hidden={!revealed}
143
+ >
144
+ {actionLabel}
145
+ </button>
146
+ <div
147
+ className={`swipe-row-content ${dragging ? "swipe-row-content-dragging" : ""}`}
148
+ style={{ transform: `translateX(${currentOffset}px)` }}
149
+ onPointerDown={handlePointerDown}
150
+ onPointerMove={handlePointerMove}
151
+ onPointerUp={handlePointerUp}
152
+ onPointerCancel={handlePointerUp}
153
+ onClickCapture={handleClickCapture}
154
+ onClick={onClick}
155
+ >
156
+ {children}
157
+ </div>
158
+ </div>
159
+ );
160
+ }
@@ -23,7 +23,9 @@ export default function TaskCard({ task, lastEvent, onEdit, onDelete, onViewRun
23
23
  const menuRef = useRef<HTMLDivElement>(null);
24
24
 
25
25
  const isRunning = lastEvent?.running_state === "started";
26
- const scheduleActive = !!task.schedule_enabled && !!task.schedule_type && (task.schedule_values?.length ?? 0) > 0;
26
+ const hasScheduleValues = (task.schedule_values?.length ?? 0) > 0;
27
+ const isEventSchedule = task.schedule_type === "on_new_notification" || task.schedule_type === "on_new_sms";
28
+ const scheduleActive = !!task.schedule_enabled && !!task.schedule_type && (hasScheduleValues || isEventSchedule);
27
29
  const stateColor =
28
30
  !scheduleActive
29
31
  ? "var(--color-text-secondary)"
@@ -126,8 +128,14 @@ export default function TaskCard({ task, lastEvent, onEdit, onDelete, onViewRun
126
128
  return `${c.kind.charAt(0).toUpperCase() + c.kind.slice(1)}: ${c.detail}`;
127
129
  }
128
130
 
129
- function formatScheduleGrouped(scheduleType: "crons" | "specific_times" | undefined, values: string[] | undefined): string {
130
- if (!scheduleType || !values || values.length === 0) return "";
131
+ function formatScheduleGrouped(
132
+ scheduleType: "crons" | "specific_times" | "on_new_notification" | "on_new_sms" | undefined,
133
+ values: string[] | undefined,
134
+ ): string {
135
+ if (!scheduleType) return "";
136
+ if (scheduleType === "on_new_notification") return "On new push notification";
137
+ if (scheduleType === "on_new_sms") return "On new SMS";
138
+ if (!values || values.length === 0) return "";
131
139
  if (values.length === 1) return formatSingleValue(scheduleType, values[0]);
132
140
 
133
141
  const classified = values.map((v) => classifyValue(scheduleType, v));
@@ -214,7 +222,7 @@ export default function TaskCard({ task, lastEvent, onEdit, onDelete, onViewRun
214
222
  {" "}{formatTime(lastEvent.time_stamp)}
215
223
  </span>
216
224
  )}
217
- {task.schedule_type && (task.schedule_values?.length ?? 0) > 0 && (
225
+ {task.schedule_type && (hasScheduleValues || isEventSchedule) && (
218
226
  <span className="task-card-triggers">
219
227
  {task.schedule_enabled ? scheduleText : "Schedule disabled"}
220
228
  </span>