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,542 +0,0 @@
1
- import { useState, useEffect } from "react";
2
- import { createPortal } from "react-dom";
3
- import { useNavigate, useLocation, useParams } from "react-router-dom";
4
-
5
- import { useHostStore } from "../contexts/HostStoreContext";
6
- import { useHostConnection } from "../contexts/HostConnectionContext";
7
- import TasksView from "../components/TasksView";
8
- import TabBar from "../components/TabBar";
9
- import HostMenu from "../components/HostMenu";
10
- import ConnectionStatusIcon from "../components/ConnectionStatusIcon";
11
- import SessionsView from "../components/SessionsView";
12
- import RunDetailView from "../components/RunDetailView";
13
- import { loadEnabledCapabilities } from "../components/CapabilityToggles";
14
- import { usePushSubscription } from "../hooks/usePushSubscription";
15
- import { useMediaQuery } from "../hooks/useMediaQuery";
16
- import { setAgentLabels } from "../agentLabels";
17
- import { confirmLeaveDraft } from "../draftGuard";
18
- import { MIN_HOST_VERSION } from "../constants";
19
- import type { AgentInfo, RequiredPermission } from "../types";
20
-
21
- function isOlderThan(current: string, minimum: string): boolean {
22
- if (current.includes("-")) return false;
23
- const a = current.split(".").map(Number);
24
- const b = minimum.split(".").map(Number);
25
- for (let i = 0; i < 3; i++) {
26
- if ((a[i] ?? 0) < (b[i] ?? 0)) return true;
27
- if ((a[i] ?? 0) > (b[i] ?? 0)) return false;
28
- }
29
- return false;
30
- }
31
-
32
- interface PendingPrompt {
33
- key: string;
34
- type: "confirmation" | "permission" | "input";
35
- params?: RequiredPermission[] | string[];
36
- meta?: {
37
- session_id?: string;
38
- session_name?: string;
39
- description?: string;
40
- input_questions?: string[];
41
- };
42
- }
43
-
44
- interface ConfirmPrompt { description: string; sessionName?: string }
45
- interface PermissionPrompt { permissions: RequiredPermission[]; sessionName?: string }
46
- interface InputPrompt { questions: string[]; description?: string; sessionName?: string }
47
-
48
- export default function Dashboard() {
49
- const { removePairedHost, setHostLanUrl, setHostTimezone } = useHostStore();
50
- const { connected, request, subscribeEvents, unauthorized, activeHost } = useHostConnection();
51
- const hostId = activeHost.hostId;
52
- const activeClientToken = activeHost.clientToken || null;
53
- const navigate = useNavigate();
54
- const location = useLocation();
55
- const params = useParams<{ taskId?: string; runId?: string }>();
56
- const isDesktop = useMediaQuery("(min-width: 768px)");
57
-
58
- const isTasksTab = location.pathname.endsWith("/tasks");
59
- const runsFilterTaskId = params.runId ? undefined : params.taskId;
60
-
61
- // "latest" is passed through to RunDetailView, which does its own resolution
62
- // and renders an empty state if the task has no runs.
63
- const isRunDetail = !!(params.taskId && params.runId);
64
-
65
- const [updateRequired, setUpdateRequired] = useState(false);
66
- const [updating, setUpdating] = useState(false);
67
- const [updateError, setUpdateError] = useState<string | null>(null);
68
- const [daemonVersion, setDaemonVersion] = useState<string | null>(null);
69
- const [linkedClientToken, setLinkedClientToken] = useState<string | null>(null);
70
- const [enabledCapabilities, setEnabledCapabilities] = useState<Set<string>>(new Set());
71
- const [agents, setAgents] = useState<AgentInfo[]>([]);
72
- const [hostPlatform, setHostPlatform] = useState<string | undefined>();
73
-
74
- const isLinkedDevice = !!activeClientToken && linkedClientToken === activeClientToken;
75
-
76
- // Pending prompt state — owned by Dashboard because these modals must show
77
- // regardless of which tab (Sessions/Tasks/RunDetail) is currently rendered.
78
- const [pendingConfirms, setPendingConfirms] = useState<Map<string, ConfirmPrompt>>(new Map());
79
- const [pendingPermissions, setPendingPermissions] = useState<Map<string, PermissionPrompt>>(new Map());
80
- const [pendingInputs, setPendingInputs] = useState<Map<string, InputPrompt>>(new Map());
81
- const [inputValues, setInputValues] = useState<Map<string, string[]>>(new Map());
82
-
83
- usePushSubscription();
84
-
85
- useEffect(() => {
86
- window.scrollTo(0, 0);
87
- }, [hostId]);
88
-
89
- useEffect(() => {
90
- let cancelled = false;
91
- loadEnabledCapabilities().then((caps) => { if (!cancelled) setEnabledCapabilities(caps); });
92
- return () => { cancelled = true; };
93
- }, []);
94
-
95
- // host.info bootstrap: agents/version/platform + any prompts that were already
96
- // pending when this PWA connected. Runs once per (host, connection).
97
- useEffect(() => {
98
- if (!connected) return;
99
- request<{
100
- agents?: AgentInfo[];
101
- version?: string | null;
102
- host_platform?: string;
103
- host_timezone?: string;
104
- linked_client_token?: string | null;
105
- pending_prompts?: PendingPrompt[];
106
- lan_url?: string | null;
107
- }>("host.info")
108
- .then((result) => {
109
- setAgents(result.agents ?? []);
110
- setHostPlatform(result.host_platform);
111
- setLinkedClientToken(result.linked_client_token ?? null);
112
- setAgentLabels(result.agents ?? []);
113
- const version = result.version ?? null;
114
- setDaemonVersion(version);
115
- setUpdateRequired(!!version && isOlderThan(version, MIN_HOST_VERSION));
116
- setHostLanUrl(hostId, result.lan_url ?? undefined);
117
- setHostTimezone(hostId, result.host_timezone);
118
-
119
- // Seed modal state from already-pending prompts.
120
- const confirms = new Map<string, ConfirmPrompt>();
121
- const perms = new Map<string, PermissionPrompt>();
122
- const inputs = new Map<string, InputPrompt>();
123
- const inputVals = new Map<string, string[]>();
124
- for (const p of result.pending_prompts ?? []) {
125
- if (p.type === "confirmation") {
126
- confirms.set(p.key, {
127
- description: p.meta?.description ?? "",
128
- sessionName: p.meta?.session_name,
129
- });
130
- } else if (p.type === "permission") {
131
- perms.set(p.key, {
132
- permissions: (p.params as RequiredPermission[]) ?? [],
133
- sessionName: p.meta?.session_name,
134
- });
135
- } else if (p.type === "input") {
136
- const questions = (p.params as string[]) ?? p.meta?.input_questions ?? [];
137
- inputs.set(p.key, {
138
- questions,
139
- description: p.meta?.description,
140
- sessionName: p.meta?.session_name,
141
- });
142
- inputVals.set(p.key, new Array(questions.length).fill(""));
143
- }
144
- }
145
- setPendingConfirms(confirms);
146
- setPendingPermissions(perms);
147
- setPendingInputs(inputs);
148
- setInputValues(inputVals);
149
- })
150
- .catch(() => { /* silent — update-required prompt guards the broken case */ });
151
- }, [connected, hostId, request, setHostLanUrl, setHostTimezone]);
152
-
153
- // Always-on event subscription for modal lifecycle. Independent of which tab
154
- // is active. Task-card status updates happen inside TasksView while mounted.
155
- useEffect(() => {
156
- if (!connected) return;
157
- const unsubscribe = subscribeEvents(hostId, (msg) => {
158
- const tokens = msg.subject.split(".");
159
- if (tokens.length < 3) return;
160
- const taskId = tokens.slice(2).join(".");
161
-
162
- let parsed: Record<string, unknown> = {};
163
- try {
164
- parsed = JSON.parse(new TextDecoder().decode(msg.data)) as Record<string, unknown>;
165
- } catch { return; }
166
- const eventType = parsed.event_type as string | undefined;
167
- const sessionId = parsed.session_id as string | undefined;
168
-
169
- if (eventType === "input-request" && sessionId) {
170
- const questions = parsed.input_questions as string[] | undefined;
171
- const sessionName = parsed.session_name as string | undefined;
172
- const description = parsed.description as string | undefined;
173
- if (questions?.length) {
174
- setPendingInputs((prev) => {
175
- if (prev.has(sessionId)) return prev;
176
- const next = new Map(prev);
177
- next.set(sessionId, { questions, description, sessionName });
178
- return next;
179
- });
180
- setInputValues((prev) => {
181
- if (prev.has(sessionId)) return prev;
182
- const next = new Map(prev);
183
- next.set(sessionId, new Array(questions.length).fill(""));
184
- return next;
185
- });
186
- }
187
- return;
188
- }
189
-
190
- if (eventType === "input-resolved" && sessionId) {
191
- setPendingInputs((prev) => {
192
- if (!prev.has(sessionId)) return prev;
193
- const next = new Map(prev);
194
- next.delete(sessionId);
195
- return next;
196
- });
197
- setInputValues((prev) => {
198
- const next = new Map(prev);
199
- next.delete(sessionId);
200
- return next;
201
- });
202
- return;
203
- }
204
-
205
- if (eventType === "confirm-request" && sessionId) {
206
- const description = parsed.description as string | undefined;
207
- const sessionName = parsed.session_name as string | undefined;
208
- if (description) {
209
- setPendingConfirms((prev) => {
210
- if (prev.has(sessionId)) return prev;
211
- const next = new Map(prev);
212
- next.set(sessionId, { description, sessionName });
213
- return next;
214
- });
215
- }
216
- return;
217
- }
218
-
219
- if (eventType === "confirm-resolved" && sessionId) {
220
- setPendingConfirms((prev) => {
221
- if (!prev.has(sessionId)) return prev;
222
- const next = new Map(prev);
223
- next.delete(sessionId);
224
- return next;
225
- });
226
- return;
227
- }
228
-
229
- if (eventType === "permission-request") {
230
- const permissions = parsed.required_permissions as RequiredPermission[] | undefined;
231
- const sessionName = parsed.session_name as string | undefined;
232
- if (permissions?.length) {
233
- setPendingPermissions((prev) => {
234
- if (prev.has(taskId)) return prev;
235
- const next = new Map(prev);
236
- next.set(taskId, { permissions, sessionName });
237
- return next;
238
- });
239
- }
240
- return;
241
- }
242
-
243
- if (eventType === "permission-resolved") {
244
- setPendingPermissions((prev) => {
245
- if (!prev.has(taskId)) return prev;
246
- const next = new Map(prev);
247
- next.delete(taskId);
248
- return next;
249
- });
250
- return;
251
- }
252
- });
253
- return unsubscribe;
254
- }, [connected, hostId, subscribeEvents]);
255
-
256
- async function respondToConfirm(sessionId: string, response: "confirmed" | "aborted") {
257
- try {
258
- await request("task.user_input", { id: sessionId, value: [response] });
259
- } catch (err) {
260
- console.error("[Dashboard] Failed to respond to confirmation:", err);
261
- }
262
- }
263
-
264
- async function respondToPermission(taskId: string, response: "granted" | "granted_all" | "aborted") {
265
- try {
266
- await request("task.user_input", { id: taskId, value: [response] });
267
- } catch (err) {
268
- console.error("[Dashboard] Failed to respond to permission request:", err);
269
- }
270
- }
271
-
272
- async function respondToInput(sessionId: string, values: string[]) {
273
- try {
274
- await request("task.user_input", { id: sessionId, value: values });
275
- } catch (err) {
276
- console.error("[Dashboard] Failed to respond to input request:", err);
277
- }
278
- }
279
-
280
- function handleViewRun(taskId: string, runId?: string) {
281
- if (!confirmLeaveDraft()) return;
282
- const base = `/hosts/${encodeURIComponent(hostId)}/runs/${encodeURIComponent(taskId)}`;
283
- navigate(runId ? `${base}/${encodeURIComponent(runId)}` : base);
284
- }
285
-
286
- async function handleUpdate() {
287
- setUpdating(true);
288
- setUpdateError(null);
289
- try {
290
- const result = await request<{ ok?: boolean; error?: string }>("host.update");
291
- if (result.error) {
292
- setUpdateError(result.error);
293
- setUpdating(false);
294
- return;
295
- }
296
- } catch {
297
- // Expected: connection drops during daemon restart
298
- }
299
- setTimeout(() => window.location.reload(), 15000);
300
- }
301
-
302
- const showTaskContent = connected && !unauthorized;
303
-
304
- return (
305
- <div className="dashboard">
306
- {isDesktop && <HostMenu daemonVersion={daemonVersion} linkedClientToken={linkedClientToken} request={request} onEnabledCapabilitiesChange={setEnabledCapabilities} onLinkedClientTokenChange={setLinkedClientToken} />}
307
-
308
- <div className="dashboard-content">
309
- <header className="app-header">
310
- <div className="app-title-bar">
311
- {!isDesktop && <HostMenu daemonVersion={daemonVersion} linkedClientToken={linkedClientToken} request={request} onEnabledCapabilitiesChange={setEnabledCapabilities} onLinkedClientTokenChange={setLinkedClientToken} />}
312
- <h1 className="app-title">Palmier</h1>
313
- <ConnectionStatusIcon />
314
- </div>
315
- <div className="tab-bar">
316
- <TabBar />
317
- </div>
318
- </header>
319
-
320
- <main className="dashboard-main">
321
- {unauthorized ? (
322
- <div className="revoked-state">
323
- <div className="revoked-icon">
324
- <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
325
- <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
326
- <path d="M7 11V7a5 5 0 0 1 10 0v4" />
327
- <line x1="12" y1="15" x2="12" y2="18" />
328
- </svg>
329
- </div>
330
- <h2 className="revoked-title">Client Revoked</h2>
331
- <p className="revoked-description">
332
- This client was revoked by the host. To reconnect, generate a new pairing code on the host machine.
333
- </p>
334
- <div className="revoked-command">
335
- <code>palmier pair</code>
336
- </div>
337
- <div className="revoked-actions">
338
- <button
339
- className="btn btn-primary"
340
- onClick={() => { if (confirmLeaveDraft()) navigate("/pair"); }}
341
- >
342
- Re-pair Device
343
- </button>
344
- <button
345
- className="btn btn-secondary"
346
- onClick={() => { removePairedHost(hostId); navigate("/", { replace: true }); }}
347
- >
348
- Remove Host
349
- </button>
350
- </div>
351
- </div>
352
- ) : showTaskContent ? (
353
- <>
354
- {isTasksTab && !isRunDetail && (
355
- <TasksView
356
- connected={connected}
357
- hostId={hostId}
358
- request={request}
359
- subscribeEvents={subscribeEvents}
360
- agents={agents}
361
- hostPlatform={hostPlatform}
362
- isNotificationListener={isLinkedDevice && enabledCapabilities.has("notifications")}
363
- onViewRun={handleViewRun}
364
- />
365
- )}
366
- {isRunDetail ? (
367
- <RunDetailView
368
- connected={connected}
369
- hostId={hostId}
370
- request={request}
371
- subscribeEvents={subscribeEvents}
372
- taskId={params.taskId!}
373
- runId={decodeURIComponent(params.runId!)}
374
- />
375
- ) : !isTasksTab ? (
376
- <SessionsView
377
- connected={connected}
378
- hostId={hostId}
379
- request={request}
380
- subscribeEvents={subscribeEvents}
381
- agents={agents}
382
- hostPlatform={hostPlatform}
383
- filterTaskId={runsFilterTaskId}
384
- onClearFilter={() => { if (confirmLeaveDraft()) navigate(`/hosts/${encodeURIComponent(hostId)}`); }}
385
- />
386
- ) : null}
387
- </>
388
- ) : (
389
- <div className="empty-state">
390
- <p>Connecting to host...</p>
391
- </div>
392
- )}
393
- </main>
394
-
395
- {updateRequired && !updating && !updateError && (
396
- <div className="confirm-modal-overlay">
397
- <div className="confirm-modal">
398
- <h2 className="confirm-modal-title">Update Required</h2>
399
- <p className="confirm-modal-message">
400
- Your Palmier host{daemonVersion ? ` (v${daemonVersion})` : ""} is too old for this version of the app. Please update to continue.
401
- </p>
402
- <div className="confirm-modal-actions">
403
- <button className="btn btn-primary" onClick={handleUpdate}>
404
- Update Now
405
- </button>
406
- </div>
407
- </div>
408
- </div>
409
- )}
410
- {updating && (
411
- <div className="confirm-modal-overlay">
412
- <div className="confirm-modal">
413
- <h2 className="confirm-modal-title">Updating...</h2>
414
- <p className="confirm-modal-message">
415
- Installing update and restarting daemon. Please wait...
416
- </p>
417
- </div>
418
- </div>
419
- )}
420
- {updateError && (
421
- <div className="confirm-modal-overlay">
422
- <div className="confirm-modal">
423
- <h2 className="confirm-modal-title">Update Failed</h2>
424
- <p className="confirm-modal-message" style={{ whiteSpace: "pre-line" }}>
425
- {updateError}
426
- </p>
427
- <div className="confirm-modal-actions">
428
- <button className="btn btn-secondary" onClick={() => { setUpdateError(null); }}>
429
- Retry
430
- </button>
431
- </div>
432
- </div>
433
- </div>
434
- )}
435
-
436
- </div>
437
-
438
- {createPortal(<>
439
- {[...pendingConfirms.entries()].map(([sessionId, { description, sessionName }]) => (
440
- <div key={sessionId} className="confirm-modal-overlay">
441
- <div className="confirm-modal">
442
- <h2 className="confirm-modal-title">Confirmation Required</h2>
443
- {sessionName && <p className="confirm-modal-subtitle">{sessionName}</p>}
444
- <p className="confirm-modal-message">{description}</p>
445
- <div className="confirm-modal-actions">
446
- <button className="btn btn-primary" onClick={() => respondToConfirm(sessionId, "confirmed")}>
447
- Confirm
448
- </button>
449
- <button className="btn btn-secondary" onClick={() => respondToConfirm(sessionId, "aborted")}>
450
- Abort
451
- </button>
452
- </div>
453
- </div>
454
- </div>
455
- ))}
456
-
457
- {[...pendingPermissions.entries()].map(([taskId, { permissions, sessionName }]) => (
458
- <div key={taskId} className="confirm-modal-overlay">
459
- <div className="confirm-modal permission-modal">
460
- <h2 className="confirm-modal-title">Permission Required</h2>
461
- <p className="confirm-modal-message">
462
- <strong>{sessionName || taskId}</strong>
463
- </p>
464
- <div className="permission-list">
465
- {permissions.map((p, i) => (
466
- <div key={i} className="permission-item">
467
- <span className="permission-name">{p.name}</span>
468
- {p.description && <span className="permission-desc">{p.description}</span>}
469
- </div>
470
- ))}
471
- </div>
472
- <div className="permission-actions">
473
- <button className="btn btn-primary" onClick={() => respondToPermission(taskId, "granted")}>
474
- Allow Once
475
- </button>
476
- <button className="btn btn-secondary" onClick={() => respondToPermission(taskId, "granted_all")}>
477
- Allow Always
478
- </button>
479
- </div>
480
- <button
481
- className="permission-abort-link"
482
- onClick={() => respondToPermission(taskId, "aborted")}
483
- >
484
- Deny & Abort Task
485
- </button>
486
- </div>
487
- </div>
488
- ))}
489
-
490
- {[...pendingInputs.entries()].map(([sessionId, { questions, description, sessionName }]) => {
491
- const values = inputValues.get(sessionId) ?? new Array(questions.length).fill("");
492
- return (
493
- <div key={sessionId} className="confirm-modal-overlay">
494
- <div className="confirm-modal input-modal">
495
- <h2 className="confirm-modal-title">Input Required</h2>
496
- {sessionName && <p className="confirm-modal-subtitle">{sessionName}</p>}
497
- {description && <p className="confirm-modal-message">{description}</p>}
498
- <div className="input-list">
499
- {questions.map((desc: string, i: number) => (
500
- <div key={i} className="input-item">
501
- <label className="input-label">{desc}</label>
502
- <input
503
- type="text"
504
- className="input-field"
505
- value={values[i] ?? ""}
506
- onChange={(e) => {
507
- setInputValues((prev) => {
508
- const next = new Map(prev);
509
- const arr = [...(next.get(sessionId) ?? [])];
510
- arr[i] = e.target.value;
511
- next.set(sessionId, arr);
512
- return next;
513
- });
514
- }}
515
- autoFocus={i === 0}
516
- />
517
- </div>
518
- ))}
519
- </div>
520
- <div className="input-actions">
521
- <button
522
- className="btn btn-primary"
523
- disabled={values.some((v) => !v.trim())}
524
- onClick={() => respondToInput(sessionId, values)}
525
- >
526
- Submit
527
- </button>
528
- </div>
529
- <button
530
- className="permission-abort-link"
531
- onClick={() => respondToInput(sessionId, ["aborted"])}
532
- >
533
- Cancel
534
- </button>
535
- </div>
536
- </div>
537
- );
538
- })}
539
- </>, document.body)}
540
- </div>
541
- );
542
- }