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.
- package/README.md +28 -13
- package/dist/agents/agent.d.ts +0 -1
- package/dist/agents/agent.js +0 -1
- package/dist/agents/aider.d.ts +0 -1
- package/dist/agents/aider.js +0 -1
- package/dist/agents/claude.d.ts +0 -1
- package/dist/agents/claude.js +0 -1
- package/dist/agents/cline.d.ts +0 -1
- package/dist/agents/cline.js +0 -1
- package/dist/agents/codex.d.ts +0 -1
- package/dist/agents/codex.js +0 -1
- package/dist/agents/copilot.d.ts +0 -1
- package/dist/agents/copilot.js +0 -1
- package/dist/agents/cursor.d.ts +0 -1
- package/dist/agents/cursor.js +0 -1
- package/dist/agents/deepagents.d.ts +0 -1
- package/dist/agents/deepagents.js +0 -1
- package/dist/agents/droid.d.ts +0 -1
- package/dist/agents/droid.js +0 -1
- package/dist/agents/gemini.d.ts +0 -1
- package/dist/agents/gemini.js +0 -1
- package/dist/agents/goose.d.ts +0 -1
- package/dist/agents/goose.js +0 -1
- package/dist/agents/hermes.d.ts +0 -1
- package/dist/agents/hermes.js +0 -1
- package/dist/agents/kimi.d.ts +0 -1
- package/dist/agents/kimi.js +0 -1
- package/dist/agents/kiro.d.ts +0 -1
- package/dist/agents/kiro.js +0 -1
- package/dist/agents/openclaw.d.ts +0 -1
- package/dist/agents/openclaw.js +0 -1
- package/dist/agents/opencode.d.ts +0 -1
- package/dist/agents/opencode.js +0 -1
- package/dist/agents/qoder.d.ts +0 -1
- package/dist/agents/qoder.js +0 -1
- package/dist/agents/qwen.d.ts +0 -1
- package/dist/agents/qwen.js +0 -1
- package/dist/agents/shared-prompt.d.ts +0 -1
- package/dist/agents/shared-prompt.js +0 -1
- package/dist/client-store.d.ts +0 -1
- package/dist/client-store.js +0 -1
- package/dist/commands/clients.d.ts +0 -1
- package/dist/commands/clients.js +0 -1
- package/dist/commands/info.d.ts +0 -1
- package/dist/commands/info.js +0 -1
- package/dist/commands/init.d.ts +0 -1
- package/dist/commands/init.js +1 -2
- package/dist/commands/pair.d.ts +0 -1
- package/dist/commands/pair.js +0 -1
- package/dist/commands/restart.d.ts +0 -1
- package/dist/commands/restart.js +0 -1
- package/dist/commands/run.d.ts +0 -1
- package/dist/commands/run.js +19 -3
- package/dist/commands/serve.d.ts +0 -1
- package/dist/commands/serve.js +0 -1
- package/dist/commands/uninstall.d.ts +0 -1
- package/dist/commands/uninstall.js +0 -1
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -1
- package/dist/event-queues.d.ts +0 -1
- package/dist/event-queues.js +0 -1
- package/dist/events.d.ts +0 -1
- package/dist/events.js +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/linked-device.d.ts +0 -1
- package/dist/linked-device.js +0 -1
- package/dist/mcp-handler.d.ts +0 -1
- package/dist/mcp-handler.js +0 -1
- package/dist/mcp-tools.d.ts +0 -1
- package/dist/mcp-tools.js +0 -1
- package/dist/nats-client.d.ts +0 -1
- package/dist/nats-client.js +0 -1
- package/dist/network.d.ts +0 -1
- package/dist/network.js +0 -1
- package/dist/notification-store.d.ts +0 -1
- package/dist/notification-store.js +0 -1
- package/dist/pending-requests.d.ts +0 -1
- package/dist/pending-requests.js +0 -1
- package/dist/platform/index.d.ts +0 -1
- package/dist/platform/index.js +0 -1
- package/dist/platform/linux.d.ts +0 -1
- package/dist/platform/linux.js +0 -1
- package/dist/platform/macos.d.ts +0 -1
- package/dist/platform/macos.js +0 -1
- package/dist/platform/platform.d.ts +0 -1
- package/dist/platform/platform.js +0 -1
- package/dist/platform/windows.d.ts +0 -1
- package/dist/platform/windows.js +0 -1
- package/dist/pwa/assets/{index-MLEFUP3r.js → index-DWvRAUiy.js} +31 -31
- package/dist/pwa/assets/{web-B1sKCc7e.js → web-C4iZbqTC.js} +1 -1
- package/dist/pwa/assets/{web-ETD-8ZHd.js → web-CBFqJGX6.js} +1 -1
- package/dist/pwa/assets/{web-B4xEa6WO.js → web-DL4uXOpS.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/rpc-handler.d.ts +0 -1
- package/dist/rpc-handler.js +0 -1
- package/dist/sms-store.d.ts +0 -1
- package/dist/sms-store.js +0 -1
- package/dist/spawn-command.d.ts +0 -1
- package/dist/spawn-command.js +0 -1
- package/dist/task.d.ts +0 -1
- package/dist/task.js +0 -1
- package/dist/transports/http-transport.d.ts +0 -1
- package/dist/transports/http-transport.js +0 -1
- package/dist/transports/nats-transport.d.ts +0 -1
- package/dist/transports/nats-transport.js +0 -1
- package/dist/types.d.ts +0 -1
- package/dist/types.js +0 -1
- package/dist/update-checker.d.ts +0 -1
- package/dist/update-checker.js +0 -1
- package/package.json +11 -1
- package/.github/workflows/ci.yml +0 -16
- package/.github/workflows/publish.yml +0 -37
- package/CLAUDE.md +0 -22
- package/dist/pwa/apple-touch-icon.png +0 -0
- package/dist/pwa/manifest.webmanifest +0 -1
- package/dist/pwa/pwa-192x192.png +0 -0
- package/dist/pwa/pwa-512x512.png +0 -0
- package/dist/pwa/registerSW.js +0 -1
- package/dist/pwa/service-worker.js +0 -2
- package/palmier-server/.github/workflows/ci.yml +0 -21
- package/palmier-server/.github/workflows/deploy.yml +0 -38
- package/palmier-server/CLAUDE.md +0 -17
- package/palmier-server/PRODUCTION.md +0 -358
- package/palmier-server/README.md +0 -231
- package/palmier-server/nats.conf +0 -19
- package/palmier-server/package.json +0 -15
- package/palmier-server/pnpm-lock.yaml +0 -7639
- package/palmier-server/pnpm-workspace.yaml +0 -3
- package/palmier-server/pwa/index.html +0 -16
- package/palmier-server/pwa/logo/logo_20260421.png +0 -0
- package/palmier-server/pwa/package.json +0 -34
- package/palmier-server/pwa/public/apple-touch-icon.png +0 -0
- package/palmier-server/pwa/public/favicon.ico +0 -0
- package/palmier-server/pwa/public/pwa-192x192.png +0 -0
- package/palmier-server/pwa/public/pwa-512x512.png +0 -0
- package/palmier-server/pwa/src/App.css +0 -3012
- package/palmier-server/pwa/src/App.tsx +0 -59
- package/palmier-server/pwa/src/agentLabels.ts +0 -11
- package/palmier-server/pwa/src/api.ts +0 -67
- package/palmier-server/pwa/src/components/CapabilityToggles.tsx +0 -170
- package/palmier-server/pwa/src/components/ConnectionStatusIcon.tsx +0 -113
- package/palmier-server/pwa/src/components/HostMenu.tsx +0 -429
- package/palmier-server/pwa/src/components/PermissionsDialog.tsx +0 -34
- package/palmier-server/pwa/src/components/PullToRefreshIndicator.tsx +0 -46
- package/palmier-server/pwa/src/components/RunDetailView.tsx +0 -343
- package/palmier-server/pwa/src/components/SessionComposer.tsx +0 -157
- package/palmier-server/pwa/src/components/SessionsView.tsx +0 -326
- package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +0 -170
- package/palmier-server/pwa/src/components/TabBar.tsx +0 -40
- package/palmier-server/pwa/src/components/TaskCard.tsx +0 -255
- package/palmier-server/pwa/src/components/TaskForm.tsx +0 -766
- package/palmier-server/pwa/src/components/TasksView.tsx +0 -179
- package/palmier-server/pwa/src/constants.ts +0 -2
- package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +0 -432
- package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +0 -124
- package/palmier-server/pwa/src/draftGuard.ts +0 -24
- package/palmier-server/pwa/src/formatTime.ts +0 -44
- package/palmier-server/pwa/src/hooks/useBackClose.ts +0 -75
- package/palmier-server/pwa/src/hooks/useMediaQuery.ts +0 -17
- package/palmier-server/pwa/src/hooks/usePullToRefresh.ts +0 -102
- package/palmier-server/pwa/src/hooks/usePushSubscription.ts +0 -77
- package/palmier-server/pwa/src/main.tsx +0 -14
- package/palmier-server/pwa/src/native/Device.ts +0 -49
- package/palmier-server/pwa/src/pages/Dashboard.tsx +0 -542
- package/palmier-server/pwa/src/pages/PairHost.tsx +0 -232
- package/palmier-server/pwa/src/pages/PairSetup.tsx +0 -134
- package/palmier-server/pwa/src/service-worker.ts +0 -142
- package/palmier-server/pwa/src/types.ts +0 -75
- package/palmier-server/pwa/src/vite-env.d.ts +0 -11
- package/palmier-server/pwa/tsconfig.json +0 -21
- package/palmier-server/pwa/tsconfig.node.json +0 -19
- package/palmier-server/pwa/vite.config.ts +0 -47
- package/palmier-server/server/.env.example +0 -20
- package/palmier-server/server/package.json +0 -36
- package/palmier-server/server/src/db.ts +0 -44
- package/palmier-server/server/src/fcm.ts +0 -74
- package/palmier-server/server/src/index.ts +0 -688
- package/palmier-server/server/src/nats-jwt.ts +0 -299
- package/palmier-server/server/src/nats-setup.ts +0 -48
- package/palmier-server/server/src/nats.ts +0 -33
- package/palmier-server/server/src/notify.ts +0 -34
- package/palmier-server/server/src/push.ts +0 -68
- package/palmier-server/server/src/routes/device.ts +0 -224
- package/palmier-server/server/src/routes/fcm.ts +0 -64
- package/palmier-server/server/src/routes/hosts.ts +0 -56
- package/palmier-server/server/src/routes/push.ts +0 -101
- package/palmier-server/server/tsconfig.json +0 -20
- package/palmier-server/spec.md +0 -533
- package/src/agents/agent-instructions.md +0 -28
- package/src/agents/agent.ts +0 -114
- package/src/agents/aider.ts +0 -35
- package/src/agents/claude.ts +0 -39
- package/src/agents/cline.ts +0 -35
- package/src/agents/codex.ts +0 -40
- package/src/agents/copilot.ts +0 -37
- package/src/agents/cursor.ts +0 -36
- package/src/agents/deepagents.ts +0 -36
- package/src/agents/droid.ts +0 -35
- package/src/agents/gemini.ts +0 -43
- package/src/agents/goose.ts +0 -33
- package/src/agents/hermes.ts +0 -36
- package/src/agents/kimi.ts +0 -35
- package/src/agents/kiro.ts +0 -36
- package/src/agents/openclaw.ts +0 -29
- package/src/agents/opencode.ts +0 -36
- package/src/agents/qoder.ts +0 -36
- package/src/agents/qwen.ts +0 -32
- package/src/agents/shared-prompt.ts +0 -30
- package/src/client-store.ts +0 -68
- package/src/commands/clients.ts +0 -29
- package/src/commands/info.ts +0 -29
- package/src/commands/init.ts +0 -165
- package/src/commands/pair.ts +0 -137
- package/src/commands/restart.ts +0 -6
- package/src/commands/run.ts +0 -608
- package/src/commands/serve.ts +0 -211
- package/src/commands/uninstall.ts +0 -9
- package/src/config.ts +0 -36
- package/src/cross-spawn.d.ts +0 -5
- package/src/event-queues.ts +0 -41
- package/src/events.ts +0 -29
- package/src/index.ts +0 -111
- package/src/linked-device.ts +0 -52
- package/src/mcp-handler.ts +0 -200
- package/src/mcp-tools.ts +0 -839
- package/src/nats-client.ts +0 -19
- package/src/network.ts +0 -96
- package/src/notification-store.ts +0 -30
- package/src/pending-requests.ts +0 -73
- package/src/platform/index.ts +0 -20
- package/src/platform/linux.ts +0 -296
- package/src/platform/macos.ts +0 -329
- package/src/platform/platform.ts +0 -31
- package/src/platform/windows.ts +0 -299
- package/src/rpc-handler.ts +0 -691
- package/src/sms-store.ts +0 -28
- package/src/spawn-command.ts +0 -123
- package/src/task.ts +0 -343
- package/src/transports/http-transport.ts +0 -478
- package/src/transports/nats-transport.ts +0 -76
- package/src/types.ts +0 -89
- package/src/update-checker.ts +0 -40
- package/test/agent-instructions.test.ts +0 -209
- package/test/agent-output-parsing.test.ts +0 -74
- package/test/linux-cron.test.ts +0 -41
- package/test/macos-plist.test.ts +0 -112
- package/test/notification-store.test.ts +0 -57
- package/test/pairing.test.ts +0 -35
- package/test/result-state.test.ts +0 -110
- package/test/task-parsing.test.ts +0 -82
- package/test/taskrun-messages.test.ts +0 -224
- package/test/tsconfig.json +0 -9
- package/test/windows-xml.test.ts +0 -89
- 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">×</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">›</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
|
-
}
|