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,326 +0,0 @@
1
- import { useState, useEffect, useCallback, useRef } from "react";
2
- import { useNavigate } from "react-router-dom";
3
- import { useFormatTime } from "../formatTime";
4
- import { confirmLeaveDraft } from "../draftGuard";
5
- import SessionComposer from "./SessionComposer";
6
- import PullToRefreshIndicator from "./PullToRefreshIndicator";
7
- import SwipeToDeleteRow from "./SwipeToDeleteRow";
8
- import { usePullToRefresh } from "../hooks/usePullToRefresh";
9
- import type { AgentInfo, HistoryEntry } from "../types";
10
-
11
- interface SessionsViewProps {
12
- connected: boolean;
13
- hostId: string | null;
14
- request<T = unknown>(method: string, params?: unknown, opts?: { timeout?: number }): Promise<T>;
15
- subscribeEvents(hostId: string, callback: (msg: { subject: string; data: Uint8Array }) => void): () => void;
16
- agents: AgentInfo[];
17
- hostPlatform?: string;
18
- filterTaskId?: string | null;
19
- onClearFilter?: () => void;
20
- }
21
-
22
- const PAGE_SIZE = 10;
23
-
24
- export default function SessionsView({ connected, hostId, request, subscribeEvents, agents, hostPlatform, filterTaskId, onClearFilter }: SessionsViewProps) {
25
- const formatTime = useFormatTime();
26
- const [entries, setEntries] = useState<HistoryEntry[]>([]);
27
- const [total, setTotal] = useState(0);
28
- const [loading, setLoading] = useState(false);
29
- const [loadingMore, setLoadingMore] = useState(false);
30
- /** Key of the row currently showing its delete action, or null. iOS pattern — at most one at a time. */
31
- const [revealedKey, setRevealedKey] = useState<string | null>(null);
32
- const navigate = useNavigate();
33
-
34
- async function deleteEntry(entry: HistoryEntry) {
35
- const key = `${entry.task_id}:${entry.run_id}`;
36
- // Optimistic: drop from the list immediately, restore if the RPC fails.
37
- setEntries((prev) => prev.filter((e) => `${e.task_id}:${e.run_id}` !== key));
38
- setTotal((t) => Math.max(0, t - 1));
39
- setRevealedKey(null);
40
- try {
41
- await request("taskrun.delete", { task_id: entry.task_id, run_id: entry.run_id });
42
- } catch (err) {
43
- console.error("Failed to delete run:", err);
44
- setEntries((prev) => [entry, ...prev]);
45
- setTotal((t) => t + 1);
46
- }
47
- }
48
-
49
- const sentinelRef = useRef<HTMLDivElement>(null);
50
-
51
- // Build RPC params with optional task_id filter
52
- const rpcParams = useCallback((offset: number) => {
53
- const params: Record<string, unknown> = { offset, limit: PAGE_SIZE };
54
- if (filterTaskId) params.task_id = filterTaskId;
55
- return params;
56
- }, [filterTaskId]);
57
-
58
- // Fetch a page of history
59
- const loadHistory = useCallback(async (offset: number, append: boolean) => {
60
- if (!connected) return;
61
- if (append) setLoadingMore(true);
62
- else setLoading(true);
63
-
64
- try {
65
- const result = await request<{ entries?: HistoryEntry[]; total?: number }>(
66
- "taskrun.list", rpcParams(offset),
67
- );
68
- const newEntries = result.entries ?? [];
69
- setTotal(result.total ?? 0);
70
- if (append) {
71
- setEntries((prev) => {
72
- const existing = new Set(prev.map((e) => `${e.task_id}:${e.run_id}`));
73
- const deduped = newEntries.filter((e) => !existing.has(`${e.task_id}:${e.run_id}`));
74
- return [...prev, ...deduped];
75
- });
76
- } else {
77
- setEntries(newEntries);
78
- }
79
- } catch (err) {
80
- if (!(err instanceof Error && err.message === "Not connected")) {
81
- console.error("Failed to load runs:", err);
82
- }
83
- } finally {
84
- setLoading(false);
85
- setLoadingMore(false);
86
- }
87
- }, [connected, request, rpcParams]);
88
-
89
- // Initial load + reload when filter changes
90
- useEffect(() => {
91
- if (connected) {
92
- setEntries([]);
93
- setTotal(0);
94
- loadHistory(0, false);
95
- } else {
96
- setEntries([]);
97
- setTotal(0);
98
- }
99
- }, [connected, hostId, loadHistory, filterTaskId]);
100
-
101
- const ptr = usePullToRefresh({
102
- onRefresh: () => loadHistory(0, false),
103
- enabled: connected,
104
- });
105
-
106
- // Real-time: update entries on running-state events
107
- useEffect(() => {
108
- if (!connected || !hostId) return;
109
- const unsubscribe = subscribeEvents(hostId, async (msg) => {
110
- try {
111
- const parsed = JSON.parse(new TextDecoder().decode(msg.data)) as { event_type?: string; running_state?: string };
112
- if (
113
- parsed.event_type === "running-state" &&
114
- parsed.running_state && ["monitoring", "started", "finished", "failed", "aborted"].includes(parsed.running_state)
115
- ) {
116
- const result = await request<{ entries?: HistoryEntry[]; total?: number }>(
117
- "taskrun.list", rpcParams(0),
118
- );
119
- const freshEntries = result.entries ?? [];
120
- setTotal(result.total ?? 0);
121
- setEntries((prev) => {
122
- const freshMap = new Map(freshEntries.map((e) => [`${e.task_id}:${e.run_id}`, e]));
123
- const updated = prev.map((e) => {
124
- const key = `${e.task_id}:${e.run_id}`;
125
- return freshMap.get(key) ?? e;
126
- });
127
- const existingKeys = new Set(updated.map((e) => `${e.task_id}:${e.run_id}`));
128
- const newOnes = freshEntries.filter((e) => !existingKeys.has(`${e.task_id}:${e.run_id}`));
129
- return [...newOnes, ...updated];
130
- });
131
- }
132
- } catch { /* skip */ }
133
- });
134
- return unsubscribe;
135
- }, [connected, hostId, subscribeEvents, request]);
136
-
137
- // Infinite scroll via IntersectionObserver
138
- useEffect(() => {
139
- const sentinel = sentinelRef.current;
140
- if (!sentinel) return;
141
-
142
- const observer = new IntersectionObserver(
143
- (observerEntries) => {
144
- if (observerEntries[0].isIntersecting && !loadingMore && entries.length < total) {
145
- loadHistory(entries.length, true);
146
- }
147
- },
148
- { threshold: 0.1 },
149
- );
150
- observer.observe(sentinel);
151
- return () => observer.disconnect();
152
- }, [entries.length, total, loadingMore, loadHistory]);
153
-
154
-
155
- function formatDuration(start?: number, end?: number): string {
156
- if (!start || !end) return "";
157
- const seconds = Math.round((end - start) / 1000);
158
- if (seconds < 60) return `${seconds}s`;
159
- const minutes = Math.floor(seconds / 60);
160
- const remainingSeconds = seconds % 60;
161
- return `${minutes}m ${remainingSeconds}s`;
162
- }
163
-
164
- const stateLabel: Record<string, string> = {
165
- monitoring: "Monitoring",
166
- started: "Running",
167
- finished: "Finished",
168
- failed: "Failed",
169
- aborted: "Aborted",
170
- };
171
-
172
- function handleCardClick(taskId: string, runId: string) {
173
- if (!confirmLeaveDraft()) return;
174
- if (!hostId) return;
175
- navigate(`/hosts/${encodeURIComponent(hostId)}/runs/${encodeURIComponent(taskId)}/${encodeURIComponent(runId)}`);
176
- }
177
-
178
- const composer = !filterTaskId && (
179
- <SessionComposer
180
- agents={agents}
181
- hostPlatform={hostPlatform}
182
- onStarted={(taskId, runId) => {
183
- if (!hostId) return;
184
- const base = `/hosts/${encodeURIComponent(hostId)}/runs/${encodeURIComponent(taskId)}`;
185
- navigate(runId ? `${base}/${encodeURIComponent(runId)}` : base);
186
- }}
187
- />
188
- );
189
- const refreshIndicator = (
190
- <PullToRefreshIndicator pullDistance={ptr.pullDistance} refreshing={ptr.refreshing} threshold={ptr.threshold} />
191
- );
192
- const filterChip = filterTaskId && onClearFilter && (
193
- <div style={{ marginBottom: "var(--space-sm)" }}>
194
- <span className="sessions-filter-chip">
195
- Filtered by task
196
- <button onClick={onClearFilter} aria-label="Clear filter">&times;</button>
197
- </span>
198
- </div>
199
- );
200
-
201
- function stateColor(state?: string): string | undefined {
202
- if (state === "failed") return "var(--color-error)";
203
- if (state === "aborted") return "var(--color-warning, #d97706)";
204
- return undefined;
205
- }
206
-
207
- // Loading skeleton
208
- if (loading && entries.length === 0 && connected) {
209
- return (
210
- <>
211
- {refreshIndicator}
212
- {composer}
213
- <div className="task-list">
214
- {[0, 1, 2].map((i) => (
215
- <div key={i} className="task-card" style={{ pointerEvents: "none" }}>
216
- <div className="task-card-header">
217
- <div className="task-card-title-row">
218
- <div className="skeleton-line" style={{ width: `${70 + i * 10}%` }} />
219
- </div>
220
- </div>
221
- <div className="task-card-meta">
222
- <div className="skeleton-line" style={{ width: "45%" }} />
223
- </div>
224
- </div>
225
- ))}
226
- </div>
227
- </>
228
- );
229
- }
230
-
231
- // Empty / disconnected states
232
- if (!connected || (loading && entries.length === 0)) {
233
- return (
234
- <>
235
- {refreshIndicator}
236
- {composer}
237
- <div className="sessions-view">
238
- <div className="empty-state">
239
- <p className="empty-state-text">Sessions</p>
240
- <p className="empty-state-hint">Your sessions will appear here</p>
241
- </div>
242
- </div>
243
- </>
244
- );
245
- }
246
-
247
- if (entries.length === 0) {
248
- return (
249
- <>
250
- {refreshIndicator}
251
- {composer}
252
- {filterChip}
253
- <div className="sessions-view">
254
- <div className="empty-state">
255
- <p className="empty-state-text">No sessions yet</p>
256
- <p className="empty-state-hint">
257
- {filterTaskId
258
- ? "This task hasn't been executed yet. Run it from the task menu or wait for its next trigger."
259
- : "Your sessions will appear here."}
260
- </p>
261
- </div>
262
- </div>
263
- </>
264
- );
265
- }
266
-
267
- return (
268
- <>
269
- {refreshIndicator}
270
- {composer}
271
- {filterChip}
272
- <div className="task-list">
273
- {entries.map((entry, i) => {
274
- const key = `${entry.task_id}:${entry.run_id}`;
275
- return (
276
- <SwipeToDeleteRow
277
- key={`${key}-${i}`}
278
- id={key}
279
- revealedId={revealedKey}
280
- setRevealedId={setRevealedKey}
281
- onDelete={() => deleteEntry(entry)}
282
- confirmMessage="Delete this session? Its run history will be removed from the host."
283
- onClick={() => !entry.error && handleCardClick(entry.task_id, entry.run_id)}
284
- >
285
- <div className="sessions-card">
286
- <div className="sessions-card-body">
287
- <h3 className="sessions-card-name">{entry.task_name || entry.task_id}</h3>
288
- <div className="sessions-card-meta">
289
- {entry.running_state === "started" ? (
290
- <span className="status-spinner" aria-label="Running">
291
- <span />
292
- </span>
293
- ) : (
294
- <span style={{ color: stateColor(entry.running_state) }}>
295
- {stateLabel[entry.running_state ?? ""] ?? entry.running_state}
296
- </span>
297
- )}
298
- {entry.end_time && <span>{formatTime(entry.end_time)}</span>}
299
- {entry.start_time && entry.end_time && (
300
- <span style={{ color: "var(--color-muted)" }}>
301
- {formatDuration(entry.start_time, entry.end_time)}
302
- </span>
303
- )}
304
- {entry.error && (
305
- <span style={{ color: "var(--color-muted)" }}>{entry.error}</span>
306
- )}
307
- </div>
308
- </div>
309
- <span className="sessions-card-chevron">&#8250;</span>
310
- </div>
311
- </SwipeToDeleteRow>
312
- );
313
- })}
314
-
315
- {/* Sentinel for infinite scroll */}
316
- <div ref={sentinelRef} style={{ height: 1 }} />
317
-
318
- {loadingMore && (
319
- <div className="loading-state" style={{ padding: "var(--space-md)" }}>
320
- <div className="spinner" />
321
- </div>
322
- )}
323
- </div>
324
- </>
325
- );
326
- }
@@ -1,170 +0,0 @@
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
- /** Message shown in the native confirm() dialog (default "Delete this item? This can't be undone."). */
15
- confirmMessage?: string;
16
- }
17
-
18
- const REVEAL_WIDTH = 72; // px width of the action button
19
- const OPEN_THRESHOLD = REVEAL_WIDTH / 2;
20
- const AXIS_LOCK_THRESHOLD = 6; // px of horizontal travel before we claim the gesture
21
-
22
- /**
23
- * Wraps a row with swipe-left to reveal a destructive action button.
24
- * Tap the button to confirm, tap elsewhere to dismiss the reveal.
25
- *
26
- * Uses pointer events so the same code works for touch and mouse. A short
27
- * axis-lock period at the start of a drag decides whether the user is
28
- * scrolling vertically (let it through) or swiping horizontally (capture).
29
- */
30
- export default function SwipeToDeleteRow({
31
- id,
32
- revealedId,
33
- setRevealedId,
34
- onDelete,
35
- onClick,
36
- children,
37
- actionLabel = "Delete",
38
- confirmMessage = "Delete this item? This can't be undone.",
39
- }: SwipeToDeleteRowProps) {
40
- const revealed = revealedId === id;
41
- const [dragOffset, setDragOffset] = useState(0);
42
- const [dragging, setDragging] = useState(false);
43
-
44
- const startX = useRef(0);
45
- const startY = useRef(0);
46
- const axis = useRef<"x" | "y" | null>(null);
47
- const baseOffset = useRef(0); // translateX when the gesture started
48
- const movedEnough = useRef(false); // whether we should suppress the click that follows
49
- const rowRef = useRef<HTMLDivElement>(null);
50
-
51
- // Reset local drag offset whenever parent closes this row.
52
- useEffect(() => {
53
- if (!revealed) setDragOffset(0);
54
- }, [revealed]);
55
-
56
- // Close when the user taps elsewhere in the document.
57
- useEffect(() => {
58
- if (!revealed) return;
59
- function onDocPointerDown(e: Event) {
60
- if (rowRef.current?.contains(e.target as Node)) return;
61
- setRevealedId(null);
62
- }
63
- document.addEventListener("pointerdown", onDocPointerDown);
64
- return () => document.removeEventListener("pointerdown", onDocPointerDown);
65
- }, [revealed, setRevealedId]);
66
-
67
- function handlePointerDown(e: PointerEvent<HTMLDivElement>) {
68
- // Ignore non-primary buttons (right-click etc.) so we don't steal them.
69
- if (e.button !== undefined && e.button !== 0) return;
70
- startX.current = e.clientX;
71
- startY.current = e.clientY;
72
- axis.current = null;
73
- baseOffset.current = revealed ? -REVEAL_WIDTH : 0;
74
- movedEnough.current = false;
75
- }
76
-
77
- function handlePointerMove(e: PointerEvent<HTMLDivElement>) {
78
- if (e.pointerType === "mouse" && e.buttons === 0) return; // not dragging
79
- const dx = e.clientX - startX.current;
80
- const dy = e.clientY - startY.current;
81
-
82
- if (axis.current === null) {
83
- if (Math.abs(dx) < AXIS_LOCK_THRESHOLD && Math.abs(dy) < AXIS_LOCK_THRESHOLD) return;
84
- axis.current = Math.abs(dx) > Math.abs(dy) ? "x" : "y";
85
- if (axis.current === "x") {
86
- try { (e.currentTarget as Element).setPointerCapture(e.pointerId); } catch { /* unsupported */ }
87
- }
88
- }
89
- if (axis.current !== "x") return;
90
-
91
- movedEnough.current = true;
92
- if (!dragging) setDragging(true);
93
- // Clamp: can swipe left to reveal fully, a bit of rubber-band on the right.
94
- let next = baseOffset.current + dx;
95
- if (next > 0) next = next / 4;
96
- if (next < -REVEAL_WIDTH) next = -REVEAL_WIDTH + (next + REVEAL_WIDTH) / 4;
97
- setDragOffset(next);
98
- }
99
-
100
- function handlePointerUp() {
101
- if (axis.current !== "x") {
102
- setDragging(false);
103
- return;
104
- }
105
- axis.current = null;
106
- setDragging(false);
107
-
108
- const finalOffset = dragOffset;
109
- const openNow = finalOffset <= -OPEN_THRESHOLD;
110
- if (openNow) {
111
- setDragOffset(-REVEAL_WIDTH);
112
- setRevealedId(id);
113
- } else {
114
- setDragOffset(0);
115
- if (revealed) setRevealedId(null);
116
- }
117
- }
118
-
119
- function handleClickCapture(e: React.MouseEvent) {
120
- // If the gesture translated the row, treat it as a swipe — not a click.
121
- // Also absorb the click that re-hides a revealed row.
122
- if (movedEnough.current) {
123
- movedEnough.current = false;
124
- e.stopPropagation();
125
- e.preventDefault();
126
- return;
127
- }
128
- if (revealed) {
129
- e.stopPropagation();
130
- e.preventDefault();
131
- setRevealedId(null);
132
- }
133
- }
134
-
135
- const currentOffset = dragOffset !== 0 ? dragOffset : (revealed ? -REVEAL_WIDTH : 0);
136
-
137
- return (
138
- <div ref={rowRef} className="swipe-row">
139
- <button
140
- type="button"
141
- className="swipe-row-action"
142
- style={{ width: REVEAL_WIDTH }}
143
- onClick={(e) => { e.stopPropagation(); if (window.confirm(confirmMessage)) onDelete(); }}
144
- tabIndex={revealed ? 0 : -1}
145
- aria-hidden={!revealed}
146
- aria-label={actionLabel}
147
- >
148
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
149
- <polyline points="3 6 5 6 21 6" />
150
- <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
151
- <path d="M10 11v6M14 11v6" />
152
- <path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2" />
153
- </svg>
154
- <span className="swipe-row-action-label">{actionLabel}</span>
155
- </button>
156
- <div
157
- className={`swipe-row-content ${dragging ? "swipe-row-content-dragging" : ""}`}
158
- style={{ transform: `translateX(${currentOffset}px)` }}
159
- onPointerDown={handlePointerDown}
160
- onPointerMove={handlePointerMove}
161
- onPointerUp={handlePointerUp}
162
- onPointerCancel={handlePointerUp}
163
- onClickCapture={handleClickCapture}
164
- onClick={onClick}
165
- >
166
- {children}
167
- </div>
168
- </div>
169
- );
170
- }
@@ -1,40 +0,0 @@
1
- import { useNavigate, useLocation, useParams } from "react-router-dom";
2
- import { confirmLeaveDraft } from "../draftGuard";
3
-
4
- export default function TabBar() {
5
- const navigate = useNavigate();
6
- const location = useLocation();
7
- const { hostId } = useParams<{ hostId: string }>();
8
- const isTasks = location.pathname.endsWith("/tasks");
9
- const isSessions = !isTasks;
10
-
11
- function go(suffix: string) {
12
- if (!confirmLeaveDraft()) return;
13
- if (!hostId) return;
14
- navigate(`/hosts/${encodeURIComponent(hostId)}${suffix}`);
15
- }
16
-
17
- return (
18
- <>
19
- <button
20
- className={`tab-btn ${isSessions ? "tab-btn-active" : ""}`}
21
- onClick={() => go("")}
22
- >
23
- <svg className="tab-icon" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
24
- <path d="M2 8H4.5L6 4L8 12L10 6L11.5 8H14" />
25
- </svg>
26
- Sessions
27
- </button>
28
- <button
29
- className={`tab-btn ${isTasks ? "tab-btn-active" : ""}`}
30
- onClick={() => go("/tasks")}
31
- >
32
- <svg className="tab-icon" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
33
- <rect x="2" y="2" width="12" height="12" rx="2" />
34
- <path d="M5.5 8L7 9.5L10.5 6" />
35
- </svg>
36
- Tasks
37
- </button>
38
- </>
39
- );
40
- }