palmier 0.9.5 → 0.9.7

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