palmier 0.9.6 → 0.9.8

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