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.
- package/CLAUDE.md +13 -0
- package/README.md +11 -11
- package/dist/agents/agent.d.ts +0 -4
- package/dist/agents/claude.js +1 -1
- package/dist/agents/codex.js +2 -2
- package/dist/agents/cursor.js +1 -1
- package/dist/agents/deepagents.js +1 -1
- package/dist/agents/gemini.js +3 -2
- package/dist/agents/goose.js +1 -1
- package/dist/agents/hermes.js +1 -1
- package/dist/agents/kiro.js +1 -1
- package/dist/agents/opencode.js +1 -1
- package/dist/agents/qoder.js +1 -1
- package/dist/agents/shared-prompt.d.ts +0 -3
- package/dist/agents/shared-prompt.js +0 -3
- package/dist/app-registry.d.ts +10 -0
- package/dist/app-registry.js +44 -0
- package/dist/commands/info.d.ts +0 -3
- package/dist/commands/info.js +0 -5
- package/dist/commands/init.d.ts +0 -3
- package/dist/commands/init.js +2 -11
- package/dist/commands/pair.d.ts +1 -4
- package/dist/commands/pair.js +1 -12
- package/dist/commands/restart.d.ts +0 -3
- package/dist/commands/restart.js +0 -3
- package/dist/commands/run.d.ts +1 -14
- package/dist/commands/run.js +18 -61
- package/dist/commands/serve.d.ts +0 -3
- package/dist/commands/serve.js +33 -27
- package/dist/config.d.ts +0 -8
- package/dist/config.js +0 -8
- package/dist/device-capabilities.d.ts +1 -1
- package/dist/event-queues.d.ts +6 -21
- package/dist/event-queues.js +6 -21
- package/dist/events.d.ts +0 -6
- package/dist/events.js +1 -9
- package/dist/index.js +0 -1
- package/dist/mcp-handler.js +1 -2
- package/dist/mcp-tools.d.ts +0 -3
- package/dist/mcp-tools.js +14 -18
- package/dist/nats-client.d.ts +0 -3
- package/dist/nats-client.js +1 -4
- package/dist/pending-requests.d.ts +4 -18
- package/dist/pending-requests.js +4 -18
- package/dist/platform/index.d.ts +1 -4
- package/dist/platform/index.js +1 -4
- package/dist/platform/linux.d.ts +3 -9
- package/dist/platform/linux.js +9 -20
- package/dist/platform/platform.d.ts +1 -4
- package/dist/platform/windows.d.ts +2 -5
- package/dist/platform/windows.js +19 -39
- package/dist/pwa/assets/index-B0F9mtid.css +1 -0
- package/dist/pwa/assets/index-SYs3mcdJ.js +120 -0
- package/dist/pwa/assets/{web-CF-N8Di6.js → web-C6lkQj9J.js} +1 -1
- package/dist/pwa/assets/{web-BpM3fNCn.js → web-Z1623me-.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.d.ts +0 -6
- package/dist/rpc-handler.js +19 -48
- package/dist/spawn-command.d.ts +10 -25
- package/dist/spawn-command.js +7 -15
- package/dist/task.d.ts +6 -64
- package/dist/task.js +7 -70
- package/dist/transports/http-transport.d.ts +0 -4
- package/dist/transports/http-transport.js +6 -28
- package/dist/transports/nats-transport.d.ts +0 -4
- package/dist/transports/nats-transport.js +3 -9
- package/dist/types.d.ts +3 -7
- package/dist/update-checker.d.ts +1 -4
- package/dist/update-checker.js +2 -5
- package/package.json +1 -1
- package/palmier-server/README.md +1 -1
- package/palmier-server/pwa/src/App.css +170 -20
- package/palmier-server/pwa/src/App.tsx +15 -1
- package/palmier-server/pwa/src/components/HostMenu.tsx +282 -473
- package/palmier-server/pwa/src/components/RunDetailView.tsx +3 -3
- package/palmier-server/pwa/src/components/SessionsView.tsx +57 -25
- package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +160 -0
- package/palmier-server/pwa/src/components/TaskCard.tsx +12 -4
- package/palmier-server/pwa/src/components/TaskForm.tsx +230 -33
- package/palmier-server/pwa/src/components/TasksView.tsx +5 -0
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/native/Device.ts +66 -0
- package/palmier-server/pwa/src/pages/Dashboard.tsx +11 -6
- package/palmier-server/pwa/src/pages/PairHost.tsx +18 -2
- package/palmier-server/pwa/src/types.ts +1 -1
- package/palmier-server/server/src/index.ts +7 -7
- package/palmier-server/server/src/routes/device.ts +4 -4
- package/palmier-server/spec.md +47 -6
- package/src/agents/agent.ts +0 -4
- package/src/agents/claude.ts +1 -1
- package/src/agents/codex.ts +2 -2
- package/src/agents/cursor.ts +1 -1
- package/src/agents/deepagents.ts +1 -1
- package/src/agents/gemini.ts +3 -2
- package/src/agents/goose.ts +1 -1
- package/src/agents/hermes.ts +1 -1
- package/src/agents/kiro.ts +1 -1
- package/src/agents/opencode.ts +1 -1
- package/src/agents/qoder.ts +1 -1
- package/src/agents/shared-prompt.ts +0 -3
- package/src/app-registry.ts +52 -0
- package/src/commands/info.ts +0 -5
- package/src/commands/init.ts +2 -11
- package/src/commands/pair.ts +1 -12
- package/src/commands/restart.ts +0 -3
- package/src/commands/run.ts +18 -65
- package/src/commands/serve.ts +31 -27
- package/src/config.ts +0 -8
- package/src/device-capabilities.ts +4 -3
- package/src/event-queues.ts +6 -21
- package/src/events.ts +1 -9
- package/src/index.ts +0 -1
- package/src/mcp-handler.ts +1 -2
- package/src/mcp-tools.ts +14 -20
- package/src/nats-client.ts +1 -4
- package/src/pending-requests.ts +4 -18
- package/src/platform/index.ts +1 -4
- package/src/platform/linux.ts +9 -20
- package/src/platform/platform.ts +1 -4
- package/src/platform/windows.ts +19 -40
- package/src/rpc-handler.ts +20 -48
- package/src/spawn-command.ts +11 -27
- package/src/task.ts +7 -70
- package/src/transports/http-transport.ts +6 -39
- package/src/transports/nats-transport.ts +3 -9
- package/src/types.ts +3 -10
- package/src/update-checker.ts +2 -5
- package/test/task-parsing.test.ts +2 -3
- package/test/windows-xml.test.ts +11 -12
- package/dist/pwa/assets/index-FP1Mipr6.js +0 -120
- 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
|
|
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
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
<
|
|
265
|
-
{
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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">›</span>
|
|
271
304
|
</div>
|
|
272
|
-
</
|
|
273
|
-
|
|
274
|
-
|
|
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
|
|
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(
|
|
130
|
-
|
|
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 && (
|
|
225
|
+
{task.schedule_type && (hasScheduleValues || isEventSchedule) && (
|
|
218
226
|
<span className="task-card-triggers">
|
|
219
227
|
{task.schedule_enabled ? scheduleText : "Schedule disabled"}
|
|
220
228
|
</span>
|