palmier 0.7.6 → 0.7.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/dist/agents/agent.d.ts +3 -0
- package/dist/agents/agent.js +1 -1
- package/dist/agents/aider.d.ts +1 -0
- package/dist/agents/aider.js +1 -0
- package/dist/agents/claude.d.ts +1 -0
- package/dist/agents/claude.js +1 -0
- package/dist/agents/cline.d.ts +1 -0
- package/dist/agents/cline.js +1 -0
- package/dist/agents/codex.d.ts +1 -0
- package/dist/agents/codex.js +1 -0
- package/dist/agents/copilot.d.ts +1 -0
- package/dist/agents/copilot.js +1 -0
- package/dist/agents/cursor.d.ts +1 -0
- package/dist/agents/cursor.js +1 -0
- package/dist/agents/deepagents.d.ts +1 -0
- package/dist/agents/deepagents.js +1 -0
- package/dist/agents/droid.d.ts +1 -0
- package/dist/agents/droid.js +1 -0
- package/dist/agents/gemini.d.ts +1 -0
- package/dist/agents/gemini.js +1 -0
- package/dist/agents/goose.d.ts +1 -0
- package/dist/agents/goose.js +1 -0
- package/dist/agents/hermes.d.ts +1 -0
- package/dist/agents/hermes.js +1 -0
- package/dist/agents/kimi.d.ts +1 -0
- package/dist/agents/kimi.js +1 -0
- package/dist/agents/kiro.d.ts +1 -0
- package/dist/agents/kiro.js +1 -0
- package/dist/agents/openclaw.d.ts +1 -0
- package/dist/agents/openclaw.js +2 -2
- package/dist/agents/opencode.d.ts +1 -0
- package/dist/agents/opencode.js +1 -0
- package/dist/agents/qoder.d.ts +1 -0
- package/dist/agents/qoder.js +1 -0
- package/dist/agents/qwen.d.ts +1 -0
- package/dist/agents/qwen.js +1 -0
- package/dist/agents/shared-prompt.js +1 -1
- package/dist/commands/init.js +3 -2
- package/dist/commands/pair.js +3 -3
- package/dist/commands/run.js +4 -4
- package/dist/commands/serve.js +1 -1
- package/dist/config.js +2 -2
- package/dist/device-capabilities.d.ts +1 -1
- package/dist/events.js +1 -1
- package/dist/mcp-tools.js +79 -7
- package/dist/nats-client.d.ts +1 -1
- package/dist/nats-client.js +6 -3
- package/dist/pending-requests.d.ts +30 -8
- package/dist/pending-requests.js +28 -15
- package/dist/pwa/assets/index-8cTctVnD.js +120 -0
- package/dist/pwa/assets/index-CSUkBBsQ.css +1 -0
- package/dist/pwa/assets/{web-DnuoxUd4.js → web-BNr628AV.js} +1 -1
- package/dist/pwa/assets/{web-7raT3zOZ.js → web-DyQPewAi.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.js +12 -16
- package/dist/transports/http-transport.js +6 -3
- package/dist/types.d.ts +4 -1
- package/package.json +1 -1
- package/palmier-server/PRODUCTION.md +31 -28
- package/palmier-server/README.md +35 -5
- package/palmier-server/nats.conf +9 -5
- package/palmier-server/package.json +2 -1
- package/palmier-server/pnpm-lock.yaml +6 -0
- package/palmier-server/pwa/src/App.css +66 -0
- package/palmier-server/pwa/src/App.tsx +1 -0
- package/palmier-server/pwa/src/components/HostMenu.tsx +65 -2
- package/palmier-server/pwa/src/components/RunsView.tsx +48 -22
- package/palmier-server/pwa/src/components/SessionComposer.tsx +137 -0
- package/palmier-server/pwa/src/components/TabBar.tsx +17 -10
- package/palmier-server/pwa/src/components/TaskForm.tsx +11 -66
- package/palmier-server/pwa/src/components/TaskListView.tsx +17 -283
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +9 -5
- package/palmier-server/pwa/src/draftGuard.ts +24 -0
- package/palmier-server/pwa/src/pages/Dashboard.tsx +335 -12
- package/palmier-server/pwa/src/pages/PairHost.tsx +6 -3
- package/palmier-server/pwa/src/types.ts +1 -6
- package/palmier-server/server/package.json +3 -1
- package/palmier-server/server/src/index.ts +83 -2
- package/palmier-server/server/src/nats-jwt.ts +299 -0
- package/palmier-server/server/src/nats-setup.ts +48 -0
- package/palmier-server/server/src/nats.ts +12 -4
- package/palmier-server/server/src/routes/device.ts +24 -0
- package/palmier-server/server/src/routes/hosts.ts +13 -2
- package/palmier-server/spec.md +28 -14
- package/src/agents/agent.ts +5 -1
- package/src/agents/aider.ts +1 -0
- package/src/agents/claude.ts +1 -0
- package/src/agents/cline.ts +1 -0
- package/src/agents/codex.ts +1 -0
- package/src/agents/copilot.ts +1 -0
- package/src/agents/cursor.ts +1 -0
- package/src/agents/deepagents.ts +1 -0
- package/src/agents/droid.ts +1 -0
- package/src/agents/gemini.ts +1 -0
- package/src/agents/goose.ts +1 -0
- package/src/agents/hermes.ts +1 -0
- package/src/agents/kimi.ts +1 -0
- package/src/agents/kiro.ts +1 -0
- package/src/agents/openclaw.ts +2 -2
- package/src/agents/opencode.ts +1 -0
- package/src/agents/qoder.ts +1 -0
- package/src/agents/qwen.ts +1 -0
- package/src/agents/shared-prompt.ts +1 -1
- package/src/commands/init.ts +7 -5
- package/src/commands/pair.ts +3 -3
- package/src/commands/run.ts +4 -4
- package/src/commands/serve.ts +1 -1
- package/src/config.ts +2 -2
- package/src/device-capabilities.ts +1 -0
- package/src/events.ts +1 -1
- package/src/mcp-tools.ts +83 -7
- package/src/nats-client.ts +10 -3
- package/src/pending-requests.ts +47 -15
- package/src/rpc-handler.ts +13 -16
- package/src/transports/http-transport.ts +6 -3
- package/src/types.ts +4 -3
- package/test/agent-instructions.test.ts +10 -10
- package/test/pairing.test.ts +2 -2
- package/dist/pwa/assets/index-B-ByUHPS.css +0 -1
- package/dist/pwa/assets/index-uSwkmHBs.js +0 -118
|
@@ -1,89 +1,42 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback } from "react";
|
|
2
|
-
import { createPortal } from "react-dom";
|
|
3
2
|
import TaskCard from "./TaskCard";
|
|
4
3
|
import TaskForm from "./TaskForm";
|
|
5
|
-
import {
|
|
6
|
-
import type { AgentInfo, Task, TaskStatus, RequiredPermission } from "../types";
|
|
7
|
-
import { MIN_HOST_VERSION } from "../constants";
|
|
8
|
-
|
|
9
|
-
function isOlderThan(current: string, minimum: string): boolean {
|
|
10
|
-
// Dev builds (e.g. "0.3.2-dev") are never forced to update
|
|
11
|
-
if (current.includes("-")) return false;
|
|
12
|
-
const a = current.split(".").map(Number);
|
|
13
|
-
const b = minimum.split(".").map(Number);
|
|
14
|
-
for (let i = 0; i < 3; i++) {
|
|
15
|
-
if ((a[i] ?? 0) < (b[i] ?? 0)) return true;
|
|
16
|
-
if ((a[i] ?? 0) > (b[i] ?? 0)) return false;
|
|
17
|
-
}
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
4
|
+
import type { AgentInfo, Task, TaskStatus } from "../types";
|
|
20
5
|
|
|
21
6
|
interface TaskListViewProps {
|
|
22
7
|
connected: boolean;
|
|
23
8
|
hostId: string | null;
|
|
24
9
|
request<T = unknown>(method: string, params?: unknown, opts?: { timeout?: number }): Promise<T>;
|
|
25
10
|
subscribeEvents(hostId: string, callback: (msg: { subject: string; data: Uint8Array }) => void): () => void;
|
|
11
|
+
agents: AgentInfo[];
|
|
12
|
+
hostPlatform?: string;
|
|
26
13
|
onViewRun(taskId: string, runId?: string): void;
|
|
27
|
-
onUpdateRequired?(required: boolean): void;
|
|
28
|
-
onVersion?(version: string | null): void;
|
|
29
|
-
onCapabilityTokens?(tokens: Record<string, string | null>): void;
|
|
30
14
|
}
|
|
31
15
|
|
|
32
|
-
export default function TaskListView({ connected, hostId, request, subscribeEvents,
|
|
16
|
+
export default function TaskListView({ connected, hostId, request, subscribeEvents, agents, hostPlatform, onViewRun }: TaskListViewProps) {
|
|
33
17
|
const [tasks, setTasks] = useState<Task[]>([]);
|
|
34
18
|
const [loadingTasks, setLoadingTasks] = useState(false);
|
|
35
19
|
const [taskError, setTaskError] = useState<string | null>(null);
|
|
36
|
-
const [
|
|
37
|
-
const [pendingConfirms, setPendingConfirms] = useState<Map<string, { description: string; agentName?: string }>>(new Map());
|
|
38
|
-
const [pendingPermissions, setPendingPermissions] = useState<Map<string, RequiredPermission[]>>(new Map());
|
|
39
|
-
const [pendingInputs, setPendingInputs] = useState<Map<string, { questions: string[]; description?: string; agentName?: string }>>(new Map());
|
|
40
|
-
const [inputValues, setInputValues] = useState<Map<string, string[]>>(new Map());
|
|
20
|
+
const [taskStatuses, setTaskStatuses] = useState<Map<string, TaskStatus>>(new Map());
|
|
41
21
|
|
|
42
22
|
const [showForm, setShowForm] = useState(false);
|
|
43
23
|
const [editingTask, setEditingTask] = useState<Task | undefined>(undefined);
|
|
44
24
|
|
|
45
|
-
const [agents, setAgents] = useState<AgentInfo[]>([]);
|
|
46
|
-
const [hostPlatform, setHostPlatform] = useState<string | undefined>();
|
|
47
|
-
|
|
48
25
|
const closeForm = useCallback(() => { setShowForm(false); setEditingTask(undefined); }, []);
|
|
49
26
|
|
|
50
|
-
// Load tasks
|
|
51
27
|
const loadTasks = useCallback(async () => {
|
|
52
28
|
if (!connected) return;
|
|
53
29
|
setLoadingTasks(true);
|
|
54
30
|
setTaskError(null);
|
|
55
31
|
try {
|
|
56
|
-
const result = await request<{ tasks?: (Task & { status?: TaskStatus })[]
|
|
32
|
+
const result = await request<{ tasks?: (Task & { status?: TaskStatus })[] }>("task.list");
|
|
57
33
|
const taskList = result.tasks ?? [];
|
|
58
|
-
const
|
|
59
|
-
const initialConfirms = new Map<string, { description: string; agentName?: string }>();
|
|
60
|
-
const initialPerms = new Map<string, RequiredPermission[]>();
|
|
61
|
-
const initialInputs = new Map<string, { questions: string[]; description?: string; agentName?: string }>();
|
|
62
|
-
const initialInputVals = new Map<string, string[]>();
|
|
34
|
+
const initialStatuses = new Map<string, TaskStatus>();
|
|
63
35
|
for (const t of taskList) {
|
|
64
|
-
if (t.status)
|
|
65
|
-
initialEvents.set(t.id, t.status);
|
|
66
|
-
// pending_confirmation no longer comes from task.list (confirmation is sessionId-based now)
|
|
67
|
-
if (t.status.pending_permission?.length) initialPerms.set(t.id, t.status.pending_permission);
|
|
68
|
-
if (t.status.pending_input?.length) {
|
|
69
|
-
initialInputs.set(t.id, { questions: t.status.pending_input });
|
|
70
|
-
initialInputVals.set(t.id, new Array(t.status.pending_input.length).fill(""));
|
|
71
|
-
}
|
|
72
|
-
}
|
|
36
|
+
if (t.status) initialStatuses.set(t.id, t.status);
|
|
73
37
|
}
|
|
74
|
-
|
|
75
|
-
setPendingConfirms(initialConfirms);
|
|
76
|
-
setPendingPermissions(initialPerms);
|
|
77
|
-
setPendingInputs(initialInputs);
|
|
78
|
-
setInputValues(initialInputVals);
|
|
38
|
+
setTaskStatuses(initialStatuses);
|
|
79
39
|
setTasks(taskList);
|
|
80
|
-
setAgents(result.agents ?? []);
|
|
81
|
-
setHostPlatform(result.host_platform);
|
|
82
|
-
setAgentLabels(result.agents ?? []);
|
|
83
|
-
const version = result.version ?? null;
|
|
84
|
-
onVersion?.(version);
|
|
85
|
-
onCapabilityTokens?.(result.capability_tokens ?? {});
|
|
86
|
-
onUpdateRequired?.(!!version && isOlderThan(version, MIN_HOST_VERSION));
|
|
87
40
|
} catch (err) {
|
|
88
41
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
89
42
|
if (errMsg === "Not connected" || errMsg.includes("503") || errMsg.toLowerCase().includes("no responders")) {
|
|
@@ -102,10 +55,11 @@ export default function TaskListView({ connected, hostId, request, subscribeEven
|
|
|
102
55
|
else { setTasks([]); setLoadingTasks(false); }
|
|
103
56
|
}, [connected, hostId, loadTasks]);
|
|
104
57
|
|
|
105
|
-
//
|
|
58
|
+
// While mounted, keep task-card state fresh by refetching task.status on
|
|
59
|
+
// running-state / permission-resolved events. Dashboard owns the modal
|
|
60
|
+
// subscription; this one only drives the card indicators.
|
|
106
61
|
useEffect(() => {
|
|
107
62
|
if (!connected || !hostId) return;
|
|
108
|
-
const sc = { decode: (data: Uint8Array) => new TextDecoder().decode(data) };
|
|
109
63
|
const unsubscribe = subscribeEvents(hostId, async (msg) => {
|
|
110
64
|
const tokens = msg.subject.split(".");
|
|
111
65
|
if (tokens.length < 3) return;
|
|
@@ -113,127 +67,20 @@ export default function TaskListView({ connected, hostId, request, subscribeEven
|
|
|
113
67
|
|
|
114
68
|
let parsed: Record<string, unknown> = {};
|
|
115
69
|
try {
|
|
116
|
-
parsed = JSON.parse(
|
|
70
|
+
parsed = JSON.parse(new TextDecoder().decode(msg.data)) as Record<string, unknown>;
|
|
117
71
|
} catch { return; }
|
|
118
72
|
const eventType = parsed.event_type as string | undefined;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
// Handle input-request: show standalone input dialog (keyed by sessionId)
|
|
122
|
-
if (eventType === "input-request" && sessionId) {
|
|
123
|
-
const questions = parsed.input_questions as string[];
|
|
124
|
-
const agentName = parsed.agent_name as string | undefined;
|
|
125
|
-
const description = parsed.description as string | undefined;
|
|
126
|
-
if (questions?.length) {
|
|
127
|
-
setPendingInputs((prev) => {
|
|
128
|
-
if (prev.has(sessionId)) return prev;
|
|
129
|
-
const next = new Map(prev);
|
|
130
|
-
next.set(sessionId, { questions, description, agentName });
|
|
131
|
-
return next;
|
|
132
|
-
});
|
|
133
|
-
setInputValues((prev) => {
|
|
134
|
-
if (prev.has(sessionId)) return prev;
|
|
135
|
-
const next = new Map(prev);
|
|
136
|
-
next.set(sessionId, new Array(questions.length).fill(""));
|
|
137
|
-
return next;
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Handle input-resolved: close pending input dialog
|
|
144
|
-
if (eventType === "input-resolved" && sessionId) {
|
|
145
|
-
setPendingInputs((prev) => {
|
|
146
|
-
if (!prev.has(sessionId)) return prev;
|
|
147
|
-
const next = new Map(prev);
|
|
148
|
-
next.delete(sessionId);
|
|
149
|
-
return next;
|
|
150
|
-
});
|
|
151
|
-
setInputValues((prev) => {
|
|
152
|
-
const next = new Map(prev);
|
|
153
|
-
next.delete(sessionId);
|
|
154
|
-
return next;
|
|
155
|
-
});
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Handle confirm-request: show standalone confirmation dialog (keyed by sessionId)
|
|
160
|
-
if (eventType === "confirm-request" && sessionId) {
|
|
161
|
-
const description = parsed.description as string;
|
|
162
|
-
const agentName = parsed.agent_name as string | undefined;
|
|
163
|
-
if (description) {
|
|
164
|
-
setPendingConfirms((prev) => {
|
|
165
|
-
if (prev.has(sessionId)) return prev;
|
|
166
|
-
const next = new Map(prev);
|
|
167
|
-
next.set(sessionId, { description, agentName });
|
|
168
|
-
return next;
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Handle confirm-resolved: close pending confirmation dialog
|
|
175
|
-
if (eventType === "confirm-resolved" && sessionId) {
|
|
176
|
-
setPendingConfirms((prev) => {
|
|
177
|
-
if (!prev.has(sessionId)) return prev;
|
|
178
|
-
const next = new Map(prev);
|
|
179
|
-
next.delete(sessionId);
|
|
180
|
-
return next;
|
|
181
|
-
});
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Handle permission-resolved: close pending permission dialog
|
|
186
|
-
if (eventType === "permission-resolved") {
|
|
187
|
-
setPendingPermissions((prev) => {
|
|
188
|
-
if (!prev.has(taskId)) return prev;
|
|
189
|
-
const next = new Map(prev);
|
|
190
|
-
next.delete(taskId);
|
|
191
|
-
return next;
|
|
192
|
-
});
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
73
|
+
if (eventType !== "running-state" && eventType !== "permission-resolved") return;
|
|
195
74
|
|
|
196
75
|
try {
|
|
197
76
|
const status = await request<TaskStatus & { error?: string }>("task.status", { id: taskId }, { timeout: 5000 });
|
|
198
77
|
if (status.error) return;
|
|
199
|
-
|
|
200
|
-
if (status.pending_permission?.length) {
|
|
201
|
-
setPendingPermissions((prev) => {
|
|
202
|
-
if (prev.has(taskId)) return prev;
|
|
203
|
-
const next = new Map(prev);
|
|
204
|
-
next.set(taskId, status.pending_permission!);
|
|
205
|
-
return next;
|
|
206
|
-
});
|
|
207
|
-
}
|
|
78
|
+
setTaskStatuses((prev) => { const next = new Map(prev); next.set(taskId, status); return next; });
|
|
208
79
|
} catch { /* skip */ }
|
|
209
80
|
});
|
|
210
81
|
return unsubscribe;
|
|
211
82
|
}, [connected, hostId, subscribeEvents, request]);
|
|
212
83
|
|
|
213
|
-
async function respondToConfirm(sessionId: string, response: "confirmed" | "aborted") {
|
|
214
|
-
try {
|
|
215
|
-
await request("task.user_input", { id: sessionId, value: [response] });
|
|
216
|
-
} catch (err) {
|
|
217
|
-
console.error("[TaskListView] Failed to respond to confirmation:", err);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async function respondToPermission(taskId: string, response: "granted" | "granted_all" | "aborted") {
|
|
222
|
-
try {
|
|
223
|
-
await request("task.user_input", { id: taskId, value: [response] });
|
|
224
|
-
} catch (err) {
|
|
225
|
-
console.error("[TaskListView] Failed to respond to permission request:", err);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
async function respondToInput(sessionId: string, values: string[]) {
|
|
230
|
-
try {
|
|
231
|
-
await request("task.user_input", { id: sessionId, value: values });
|
|
232
|
-
} catch (err) {
|
|
233
|
-
console.error("[TaskListView] Failed to respond to input request:", err);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
84
|
function handleTaskSaved(task: Task) {
|
|
238
85
|
setTasks((prev) => {
|
|
239
86
|
const idx = prev.findIndex((t) => t.id === task.id);
|
|
@@ -246,9 +93,7 @@ export default function TaskListView({ connected, hostId, request, subscribeEven
|
|
|
246
93
|
|
|
247
94
|
function handleTaskDeleted(taskId: string) {
|
|
248
95
|
setTasks((prev) => prev.filter((t) => t.id !== taskId));
|
|
249
|
-
|
|
250
|
-
setPendingInputs((prev) => { const next = new Map(prev); next.delete(taskId); return next; });
|
|
251
|
-
setInputValues((prev) => { const next = new Map(prev); next.delete(taskId); return next; });
|
|
96
|
+
setTaskStatuses((prev) => { const next = new Map(prev); next.delete(taskId); return next; });
|
|
252
97
|
}
|
|
253
98
|
|
|
254
99
|
return (
|
|
@@ -289,7 +134,7 @@ export default function TaskListView({ connected, hostId, request, subscribeEven
|
|
|
289
134
|
<TaskCard
|
|
290
135
|
key={task.id}
|
|
291
136
|
task={task}
|
|
292
|
-
lastEvent={
|
|
137
|
+
lastEvent={taskStatuses.get(task.id)}
|
|
293
138
|
onEdit={async (t) => {
|
|
294
139
|
try {
|
|
295
140
|
const latest = await request<Task & { error?: string }>("task.get", { id: t.id });
|
|
@@ -312,120 +157,9 @@ export default function TaskListView({ connected, hostId, request, subscribeEven
|
|
|
312
157
|
agents={agents}
|
|
313
158
|
hostPlatform={hostPlatform}
|
|
314
159
|
onSaved={handleTaskSaved}
|
|
315
|
-
onRun={onViewRun}
|
|
316
160
|
onCancel={closeForm}
|
|
317
161
|
/>
|
|
318
162
|
)}
|
|
319
|
-
|
|
320
|
-
{createPortal(<>
|
|
321
|
-
{[...pendingConfirms.entries()].map(([sessionId, { description, agentName }]) => {
|
|
322
|
-
const subtitle = agentName || tasks.find((t) => t.id === sessionId)?.name;
|
|
323
|
-
return (
|
|
324
|
-
<div key={sessionId} className="confirm-modal-overlay">
|
|
325
|
-
<div className="confirm-modal">
|
|
326
|
-
<h2 className="confirm-modal-title">Confirmation Required</h2>
|
|
327
|
-
{subtitle && <p className="confirm-modal-subtitle">{subtitle}</p>}
|
|
328
|
-
<p className="confirm-modal-message">{description}</p>
|
|
329
|
-
<div className="confirm-modal-actions">
|
|
330
|
-
<button className="btn btn-primary" onClick={() => respondToConfirm(sessionId, "confirmed")}>
|
|
331
|
-
Confirm
|
|
332
|
-
</button>
|
|
333
|
-
<button className="btn btn-secondary" onClick={() => respondToConfirm(sessionId, "aborted")}>
|
|
334
|
-
Abort
|
|
335
|
-
</button>
|
|
336
|
-
</div>
|
|
337
|
-
</div>
|
|
338
|
-
</div>
|
|
339
|
-
);
|
|
340
|
-
})}
|
|
341
|
-
|
|
342
|
-
{[...pendingPermissions.entries()].map(([taskId, permissions]) => {
|
|
343
|
-
const task = tasks.find((t) => t.id === taskId);
|
|
344
|
-
return (
|
|
345
|
-
<div key={taskId} className="confirm-modal-overlay">
|
|
346
|
-
<div className="confirm-modal permission-modal">
|
|
347
|
-
<h2 className="confirm-modal-title">Permission Required</h2>
|
|
348
|
-
<p className="confirm-modal-message">
|
|
349
|
-
<strong>{task?.name || task?.user_prompt || taskId}</strong>
|
|
350
|
-
</p>
|
|
351
|
-
<div className="permission-list">
|
|
352
|
-
{permissions.map((p, i) => (
|
|
353
|
-
<div key={i} className="permission-item">
|
|
354
|
-
<span className="permission-name">{p.name}</span>
|
|
355
|
-
{p.description && <span className="permission-desc">{p.description}</span>}
|
|
356
|
-
</div>
|
|
357
|
-
))}
|
|
358
|
-
</div>
|
|
359
|
-
<div className="permission-actions">
|
|
360
|
-
<button className="btn btn-primary" onClick={() => respondToPermission(taskId, "granted")}>
|
|
361
|
-
Allow Once
|
|
362
|
-
</button>
|
|
363
|
-
<button className="btn btn-secondary" onClick={() => respondToPermission(taskId, "granted_all")}>
|
|
364
|
-
Allow Always
|
|
365
|
-
</button>
|
|
366
|
-
</div>
|
|
367
|
-
<button
|
|
368
|
-
className="permission-abort-link"
|
|
369
|
-
onClick={() => respondToPermission(taskId, "aborted")}
|
|
370
|
-
>
|
|
371
|
-
Deny & Abort Task
|
|
372
|
-
</button>
|
|
373
|
-
</div>
|
|
374
|
-
</div>
|
|
375
|
-
);
|
|
376
|
-
})}
|
|
377
|
-
|
|
378
|
-
{[...pendingInputs.entries()].map(([sessionId, { questions, description, agentName }]) => {
|
|
379
|
-
const values = inputValues.get(sessionId) ?? new Array(questions.length).fill("");
|
|
380
|
-
const subtitle = agentName || tasks.find((t) => t.id === sessionId)?.name;
|
|
381
|
-
return (
|
|
382
|
-
<div key={sessionId} className="confirm-modal-overlay">
|
|
383
|
-
<div className="confirm-modal input-modal">
|
|
384
|
-
<h2 className="confirm-modal-title">Input Required</h2>
|
|
385
|
-
{subtitle && <p className="confirm-modal-subtitle">{subtitle}</p>}
|
|
386
|
-
{description && <p className="confirm-modal-message">{description}</p>}
|
|
387
|
-
<div className="input-list">
|
|
388
|
-
{questions.map((desc: string, i: number) => (
|
|
389
|
-
<div key={i} className="input-item">
|
|
390
|
-
<label className="input-label">{desc}</label>
|
|
391
|
-
<input
|
|
392
|
-
type="text"
|
|
393
|
-
className="input-field"
|
|
394
|
-
value={values[i] ?? ""}
|
|
395
|
-
onChange={(e) => {
|
|
396
|
-
setInputValues((prev) => {
|
|
397
|
-
const next = new Map(prev);
|
|
398
|
-
const arr = [...(next.get(sessionId) ?? [])];
|
|
399
|
-
arr[i] = e.target.value;
|
|
400
|
-
next.set(sessionId, arr);
|
|
401
|
-
return next;
|
|
402
|
-
});
|
|
403
|
-
}}
|
|
404
|
-
autoFocus={i === 0}
|
|
405
|
-
/>
|
|
406
|
-
</div>
|
|
407
|
-
))}
|
|
408
|
-
</div>
|
|
409
|
-
<div className="input-actions">
|
|
410
|
-
<button
|
|
411
|
-
className="btn btn-primary"
|
|
412
|
-
disabled={values.some((v) => !v.trim())}
|
|
413
|
-
onClick={() => respondToInput(sessionId, values)}
|
|
414
|
-
>
|
|
415
|
-
Submit
|
|
416
|
-
</button>
|
|
417
|
-
</div>
|
|
418
|
-
<button
|
|
419
|
-
className="permission-abort-link"
|
|
420
|
-
onClick={() => respondToInput(sessionId, ["aborted"])}
|
|
421
|
-
>
|
|
422
|
-
Cancel
|
|
423
|
-
</button>
|
|
424
|
-
</div>
|
|
425
|
-
</div>
|
|
426
|
-
);
|
|
427
|
-
})}
|
|
428
|
-
</>, document.body)}
|
|
429
163
|
</>
|
|
430
164
|
);
|
|
431
165
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Bump when a breaking host change is made. */
|
|
2
|
-
export const MIN_HOST_VERSION = "0.7.
|
|
2
|
+
export const MIN_HOST_VERSION = "0.7.8";
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
useCallback,
|
|
8
8
|
type ReactNode,
|
|
9
9
|
} from "react";
|
|
10
|
-
import { connect, StringCodec, type NatsConnection, type Subscription } from "nats.ws";
|
|
10
|
+
import { connect, jwtAuthenticator, StringCodec, type NatsConnection, type Subscription } from "nats.ws";
|
|
11
11
|
import { SERVER_URL } from "../api";
|
|
12
12
|
import { useHostStore } from "./HostStoreContext";
|
|
13
13
|
import type { PairedHost } from "../types";
|
|
@@ -90,12 +90,13 @@ export function HostConnectionProvider({ children }: { children: ReactNode }) {
|
|
|
90
90
|
|
|
91
91
|
async function init() {
|
|
92
92
|
try {
|
|
93
|
-
|
|
93
|
+
// Fetch host-scoped NATS credentials (can only access this host's subjects)
|
|
94
|
+
const res = await fetch(`${SERVER_URL}/api/nats-credentials/${activeHost!.hostId}`);
|
|
94
95
|
if (!res.ok) {
|
|
95
|
-
console.error("[NATS] Failed to fetch
|
|
96
|
+
console.error("[NATS] Failed to fetch credentials");
|
|
96
97
|
return;
|
|
97
98
|
}
|
|
98
|
-
const config = await res.json() as { natsWsUrl: string;
|
|
99
|
+
const config = await res.json() as { natsWsUrl: string; natsJwt: string; natsNkeySeed: string };
|
|
99
100
|
if (!config.natsWsUrl) {
|
|
100
101
|
console.warn("[NATS] No WebSocket URL configured");
|
|
101
102
|
return;
|
|
@@ -105,7 +106,10 @@ export function HostConnectionProvider({ children }: { children: ReactNode }) {
|
|
|
105
106
|
console.log("[NATS] Connecting to", config.natsWsUrl);
|
|
106
107
|
const conn = await connect({
|
|
107
108
|
servers: config.natsWsUrl,
|
|
108
|
-
|
|
109
|
+
authenticator: jwtAuthenticator(
|
|
110
|
+
config.natsJwt,
|
|
111
|
+
new TextEncoder().encode(config.natsNkeySeed),
|
|
112
|
+
),
|
|
109
113
|
});
|
|
110
114
|
if (cancelled) { conn.close().catch(() => {}); return; }
|
|
111
115
|
console.log("[NATS] Connected");
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
let currentDraftMessage: string | null = null;
|
|
2
|
+
|
|
3
|
+
function handleBeforeUnload(e: BeforeUnloadEvent) {
|
|
4
|
+
if (currentDraftMessage) {
|
|
5
|
+
e.preventDefault();
|
|
6
|
+
e.returnValue = "";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function setDraftMessage(message: string | null) {
|
|
11
|
+
const wasActive = !!currentDraftMessage;
|
|
12
|
+
currentDraftMessage = message;
|
|
13
|
+
const nowActive = !!currentDraftMessage;
|
|
14
|
+
if (nowActive && !wasActive) {
|
|
15
|
+
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
16
|
+
} else if (!nowActive && wasActive) {
|
|
17
|
+
window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function confirmLeaveDraft(): boolean {
|
|
22
|
+
if (!currentDraftMessage) return true;
|
|
23
|
+
return window.confirm(currentDraftMessage);
|
|
24
|
+
}
|