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