palmier 0.6.0 → 0.6.2
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/.github/workflows/publish.yml +15 -2
- package/CLAUDE.md +2 -2
- package/DISCLAIMER.md +36 -0
- package/README.md +76 -87
- package/dist/agents/agent-instructions.md +1 -1
- package/dist/agents/agent.d.ts +2 -0
- package/dist/agents/agent.js +21 -0
- package/dist/agents/aider.d.ts +9 -0
- package/dist/agents/aider.js +32 -0
- package/dist/agents/cursor.d.ts +9 -0
- package/dist/agents/cursor.js +35 -0
- package/dist/agents/deepagents.d.ts +9 -0
- package/dist/agents/deepagents.js +35 -0
- package/dist/agents/droid.d.ts +9 -0
- package/dist/agents/droid.js +32 -0
- package/dist/agents/goose.d.ts +9 -0
- package/dist/agents/goose.js +32 -0
- package/dist/agents/opencode.d.ts +9 -0
- package/dist/agents/opencode.js +35 -0
- package/dist/agents/openhands.d.ts +9 -0
- package/dist/agents/openhands.js +35 -0
- package/dist/commands/pair.d.ts +1 -1
- package/dist/commands/pair.js +1 -1
- package/dist/commands/run.js +2 -2
- package/dist/pwa/apple-touch-icon.png +0 -0
- package/dist/pwa/assets/index-ByhOhTz1.js +118 -0
- package/dist/pwa/assets/index-_AmC1Rkn.css +1 -0
- package/dist/pwa/assets/plus-jakarta-sans-latin-ext-wght-normal-DmpS2jIq.woff2 +0 -0
- package/dist/pwa/assets/plus-jakarta-sans-latin-wght-normal-eXO_dkmS.woff2 +0 -0
- package/dist/pwa/assets/plus-jakarta-sans-vietnamese-wght-normal-qRpaaN48.woff2 +0 -0
- package/dist/pwa/favicon.ico +0 -0
- package/dist/pwa/index.html +17 -0
- package/dist/pwa/manifest.webmanifest +1 -0
- package/dist/pwa/pwa-192x192.png +0 -0
- package/dist/pwa/pwa-512x512.png +0 -0
- package/dist/pwa/registerSW.js +1 -0
- package/dist/pwa/service-worker.js +2 -0
- package/dist/rpc-handler.d.ts +4 -0
- package/dist/rpc-handler.js +5 -4
- package/dist/transports/http-transport.js +29 -41
- package/package.json +2 -2
- package/palmier-server/.github/workflows/ci.yml +21 -0
- package/palmier-server/.github/workflows/deploy.yml +38 -0
- package/palmier-server/CLAUDE.md +13 -0
- package/palmier-server/PRODUCTION.md +355 -0
- package/palmier-server/README.md +187 -0
- package/palmier-server/nats.conf +15 -0
- package/palmier-server/package.json +8 -0
- package/palmier-server/pnpm-lock.yaml +6597 -0
- package/palmier-server/pnpm-workspace.yaml +3 -0
- package/palmier-server/pwa/index.html +16 -0
- package/palmier-server/pwa/logo/logo-prompt.md +28 -0
- package/palmier-server/pwa/logo/logo_20260330.png +0 -0
- package/palmier-server/pwa/package.json +30 -0
- package/palmier-server/pwa/public/apple-touch-icon.png +0 -0
- package/palmier-server/pwa/public/favicon.ico +0 -0
- package/palmier-server/pwa/public/pwa-192x192.png +0 -0
- package/palmier-server/pwa/public/pwa-512x512.png +0 -0
- package/palmier-server/pwa/src/App.css +2387 -0
- package/palmier-server/pwa/src/App.tsx +21 -0
- package/palmier-server/pwa/src/agentLabels.ts +11 -0
- package/palmier-server/pwa/src/api.ts +61 -0
- package/palmier-server/pwa/src/components/HostMenu.tsx +289 -0
- package/palmier-server/pwa/src/components/PlanDialog.tsx +41 -0
- package/palmier-server/pwa/src/components/RunDetailView.tsx +293 -0
- package/palmier-server/pwa/src/components/RunsView.tsx +254 -0
- package/palmier-server/pwa/src/components/TabBar.tsx +31 -0
- package/palmier-server/pwa/src/components/TaskCard.tsx +213 -0
- package/palmier-server/pwa/src/components/TaskForm.tsx +580 -0
- package/palmier-server/pwa/src/components/TaskListView.tsx +415 -0
- package/palmier-server/pwa/src/constants.ts +2 -0
- package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +313 -0
- package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +135 -0
- package/palmier-server/pwa/src/formatTime.ts +10 -0
- package/palmier-server/pwa/src/hooks/useBackClose.ts +75 -0
- package/palmier-server/pwa/src/hooks/useMediaQuery.ts +17 -0
- package/palmier-server/pwa/src/hooks/usePushSubscription.ts +75 -0
- package/palmier-server/pwa/src/main.tsx +14 -0
- package/palmier-server/pwa/src/pages/Dashboard.tsx +223 -0
- package/palmier-server/pwa/src/pages/PairHost.tsx +178 -0
- package/palmier-server/pwa/src/service-worker.ts +139 -0
- package/palmier-server/pwa/src/types.ts +79 -0
- package/palmier-server/pwa/src/vite-env.d.ts +11 -0
- package/palmier-server/pwa/tsconfig.json +21 -0
- package/palmier-server/pwa/tsconfig.node.json +19 -0
- package/palmier-server/pwa/vite.config.ts +47 -0
- package/palmier-server/server/.env.example +16 -0
- package/palmier-server/server/package.json +33 -0
- package/palmier-server/server/src/db.ts +34 -0
- package/palmier-server/server/src/index.ts +219 -0
- package/palmier-server/server/src/nats.ts +25 -0
- package/palmier-server/server/src/push.ts +68 -0
- package/palmier-server/server/src/routes/hosts.ts +45 -0
- package/palmier-server/server/src/routes/push.ts +100 -0
- package/palmier-server/server/tsconfig.json +20 -0
- package/palmier-server/spec.md +415 -0
- package/src/agents/agent-instructions.md +1 -1
- package/src/agents/agent.ts +23 -0
- package/src/agents/aider.ts +37 -0
- package/src/agents/cursor.ts +38 -0
- package/src/agents/deepagents.ts +38 -0
- package/src/agents/droid.ts +37 -0
- package/src/agents/goose.ts +35 -0
- package/src/agents/opencode.ts +38 -0
- package/src/agents/openhands.ts +38 -0
- package/src/commands/pair.ts +1 -1
- package/src/commands/run.ts +2 -2
- package/src/rpc-handler.ts +5 -4
- package/src/transports/http-transport.ts +31 -43
- package/test/result-state.test.ts +110 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Routes, Route } from "react-router-dom";
|
|
2
|
+
import { HostStoreProvider } from "./contexts/HostStoreContext";
|
|
3
|
+
import { HostConnectionProvider } from "./contexts/HostConnectionContext";
|
|
4
|
+
import Dashboard from "./pages/Dashboard";
|
|
5
|
+
import PairHost from "./pages/PairHost";
|
|
6
|
+
|
|
7
|
+
export default function App() {
|
|
8
|
+
return (
|
|
9
|
+
<HostStoreProvider>
|
|
10
|
+
<HostConnectionProvider>
|
|
11
|
+
<Routes>
|
|
12
|
+
<Route path="/" element={<Dashboard />} />
|
|
13
|
+
<Route path="/runs" element={<Dashboard />} />
|
|
14
|
+
<Route path="/runs/:taskId" element={<Dashboard />} />
|
|
15
|
+
<Route path="/runs/:taskId/:runId" element={<Dashboard />} />
|
|
16
|
+
<Route path="/pair" element={<PairHost />} />
|
|
17
|
+
</Routes>
|
|
18
|
+
</HostConnectionProvider>
|
|
19
|
+
</HostStoreProvider>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AgentInfo } from "./types";
|
|
2
|
+
|
|
3
|
+
const labels: Record<string, string> = {};
|
|
4
|
+
|
|
5
|
+
export function setAgentLabels(agents: AgentInfo[]): void {
|
|
6
|
+
for (const a of agents) labels[a.key] = a.label;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getAgentLabel(key: string): string {
|
|
10
|
+
return labels[key] ?? key;
|
|
11
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
async function request<T>(
|
|
2
|
+
method: string,
|
|
3
|
+
path: string,
|
|
4
|
+
body?: unknown,
|
|
5
|
+
token?: string
|
|
6
|
+
): Promise<T> {
|
|
7
|
+
console.log(`[API] ${method} ${path}`);
|
|
8
|
+
const headers: Record<string, string> = {
|
|
9
|
+
"Content-Type": "application/json",
|
|
10
|
+
};
|
|
11
|
+
if (token) {
|
|
12
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
13
|
+
}
|
|
14
|
+
const res = await fetch(path, {
|
|
15
|
+
method,
|
|
16
|
+
headers,
|
|
17
|
+
body: body != null ? JSON.stringify(body) : undefined,
|
|
18
|
+
});
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
const text = await res.text();
|
|
21
|
+
let message: string;
|
|
22
|
+
try {
|
|
23
|
+
const json = JSON.parse(text);
|
|
24
|
+
message = json.error ?? json.message ?? text;
|
|
25
|
+
} catch {
|
|
26
|
+
message = text;
|
|
27
|
+
}
|
|
28
|
+
console.error(`[API] ${method} ${path} failed:`, res.status, message);
|
|
29
|
+
throw new Error(message || `Request failed with status ${res.status}`);
|
|
30
|
+
}
|
|
31
|
+
console.log(`[API] ${method} ${path} ->`, res.status);
|
|
32
|
+
return res.json() as Promise<T>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function apiPost<T = unknown>(
|
|
36
|
+
path: string,
|
|
37
|
+
body: unknown,
|
|
38
|
+
token?: string
|
|
39
|
+
): Promise<T> {
|
|
40
|
+
return request<T>("POST", path, body, token);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function apiGet<T = unknown>(path: string, token?: string): Promise<T> {
|
|
44
|
+
return request<T>("GET", path, undefined, token);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function apiPatch<T = unknown>(
|
|
48
|
+
path: string,
|
|
49
|
+
body: unknown,
|
|
50
|
+
token?: string
|
|
51
|
+
): Promise<T> {
|
|
52
|
+
return request<T>("PATCH", path, body, token);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function apiDelete<T = unknown>(
|
|
56
|
+
path: string,
|
|
57
|
+
body?: unknown,
|
|
58
|
+
token?: string
|
|
59
|
+
): Promise<T> {
|
|
60
|
+
return request<T>("DELETE", path, body, token);
|
|
61
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useCallback } from "react";
|
|
2
|
+
import { createPortal } from "react-dom";
|
|
3
|
+
import { useNavigate } from "react-router-dom";
|
|
4
|
+
import { useHostStore } from "../contexts/HostStoreContext";
|
|
5
|
+
import { useMediaQuery } from "../hooks/useMediaQuery";
|
|
6
|
+
|
|
7
|
+
/** LAN mode: PWA is served by palmier serve (marker injected into HTML). */
|
|
8
|
+
const isLanMode = !!(window as any).__PALMIER_SERVE__;
|
|
9
|
+
|
|
10
|
+
interface HostMenuProps {
|
|
11
|
+
daemonVersion?: string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function HostMenu({ daemonVersion }: HostMenuProps) {
|
|
15
|
+
const { pairedHosts, activeHostId, setActiveHostId, removePairedHost, renamePairedHost } = useHostStore();
|
|
16
|
+
const navigate = useNavigate();
|
|
17
|
+
const isDesktop = useMediaQuery("(min-width: 768px)");
|
|
18
|
+
|
|
19
|
+
const [visible, setVisible] = useState(false);
|
|
20
|
+
const [closing, setClosing] = useState(false);
|
|
21
|
+
const [renamingId, setRenamingId] = useState<string | null>(null);
|
|
22
|
+
const [renameValue, setRenameValue] = useState("");
|
|
23
|
+
const [confirmingDeleteId, setConfirmingDeleteId] = useState<string | null>(null);
|
|
24
|
+
const drawerRef = useRef<HTMLDivElement>(null);
|
|
25
|
+
const renameInputRef = useRef<HTMLInputElement>(null);
|
|
26
|
+
|
|
27
|
+
// In LAN mode, there's only one host — no picker/pairing needed
|
|
28
|
+
|
|
29
|
+
const close = useCallback(() => {
|
|
30
|
+
setClosing(true);
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
function handleAnimationEnd() {
|
|
34
|
+
if (closing) {
|
|
35
|
+
setVisible(false);
|
|
36
|
+
setClosing(false);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function openDrawer() {
|
|
41
|
+
setClosing(false);
|
|
42
|
+
setVisible(true);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function startRename(hostId: string, currentName: string) {
|
|
46
|
+
setRenamingId(hostId);
|
|
47
|
+
setRenameValue(currentName);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function submitRename() {
|
|
51
|
+
if (renamingId && renameValue.trim()) {
|
|
52
|
+
renamePairedHost(renamingId, renameValue.trim());
|
|
53
|
+
}
|
|
54
|
+
setRenamingId(null);
|
|
55
|
+
setRenameValue("");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function cancelRename() {
|
|
59
|
+
setRenamingId(null);
|
|
60
|
+
setRenameValue("");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (renamingId && renameInputRef.current) {
|
|
65
|
+
renameInputRef.current.focus();
|
|
66
|
+
renameInputRef.current.select();
|
|
67
|
+
}
|
|
68
|
+
}, [renamingId]);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!visible || closing) return;
|
|
72
|
+
function handleKey(e: KeyboardEvent) {
|
|
73
|
+
if (e.key === "Escape") close();
|
|
74
|
+
}
|
|
75
|
+
document.addEventListener("keydown", handleKey);
|
|
76
|
+
return () => {
|
|
77
|
+
document.removeEventListener("keydown", handleKey);
|
|
78
|
+
};
|
|
79
|
+
}, [visible, closing, close]);
|
|
80
|
+
|
|
81
|
+
const drawerContent = (
|
|
82
|
+
<>
|
|
83
|
+
<div className="drawer-header">
|
|
84
|
+
<span className="drawer-title">Palmier</span>
|
|
85
|
+
{!isDesktop && (
|
|
86
|
+
<button
|
|
87
|
+
className="drawer-close-btn"
|
|
88
|
+
onClick={close}
|
|
89
|
+
aria-label="Close menu"
|
|
90
|
+
>
|
|
91
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
92
|
+
<path d="M4 4L12 12M12 4L4 12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
93
|
+
</svg>
|
|
94
|
+
</button>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
{!isLanMode && pairedHosts.length > 0 && (
|
|
99
|
+
<div className="drawer-section">
|
|
100
|
+
<h3 className="drawer-section-label">Hosts</h3>
|
|
101
|
+
<div className="host-picker-inline">
|
|
102
|
+
<div className="host-picker-list" role="listbox">
|
|
103
|
+
{pairedHosts.map((host) => {
|
|
104
|
+
const isActive = host.hostId === activeHostId;
|
|
105
|
+
const isRenaming = renamingId === host.hostId;
|
|
106
|
+
const displayName = host.name || host.hostId.slice(0, 8);
|
|
107
|
+
return (
|
|
108
|
+
<div key={host.hostId} className="host-picker-item-wrapper">
|
|
109
|
+
<div
|
|
110
|
+
className={`host-picker-item ${isActive ? "host-picker-item-active" : ""}`}
|
|
111
|
+
onClick={() => {
|
|
112
|
+
if (isRenaming) return;
|
|
113
|
+
if (!isActive) { setActiveHostId(host.hostId); if (!isDesktop) close(); }
|
|
114
|
+
}}
|
|
115
|
+
role="option"
|
|
116
|
+
aria-selected={isActive}
|
|
117
|
+
>
|
|
118
|
+
{isRenaming ? (
|
|
119
|
+
<input
|
|
120
|
+
ref={renameInputRef}
|
|
121
|
+
className="form-input host-picker-rename-input"
|
|
122
|
+
type="text"
|
|
123
|
+
value={renameValue}
|
|
124
|
+
onChange={(e) => setRenameValue(e.target.value)}
|
|
125
|
+
onKeyDown={(e) => {
|
|
126
|
+
if (e.key === "Enter") submitRename();
|
|
127
|
+
if (e.key === "Escape") cancelRename();
|
|
128
|
+
}}
|
|
129
|
+
onBlur={submitRename}
|
|
130
|
+
onClick={(e) => e.stopPropagation()}
|
|
131
|
+
/>
|
|
132
|
+
) : (
|
|
133
|
+
<span className="host-picker-item-name">
|
|
134
|
+
{displayName}
|
|
135
|
+
</span>
|
|
136
|
+
)}
|
|
137
|
+
<div className="host-picker-item-actions">
|
|
138
|
+
{isActive && !isRenaming && (
|
|
139
|
+
<button
|
|
140
|
+
className="host-picker-edit-btn"
|
|
141
|
+
onClick={(e) => {
|
|
142
|
+
e.stopPropagation();
|
|
143
|
+
startRename(host.hostId, displayName);
|
|
144
|
+
}}
|
|
145
|
+
aria-label="Rename host"
|
|
146
|
+
>
|
|
147
|
+
<svg width="13" height="13" viewBox="0 0 13 13" fill="none">
|
|
148
|
+
<path
|
|
149
|
+
d="M9.5 1.5L11.5 3.5M1.5 11.5L2 9L9 2L11 4L4 11L1.5 11.5Z"
|
|
150
|
+
stroke="currentColor"
|
|
151
|
+
strokeWidth="1.2"
|
|
152
|
+
strokeLinecap="round"
|
|
153
|
+
strokeLinejoin="round"
|
|
154
|
+
/>
|
|
155
|
+
</svg>
|
|
156
|
+
</button>
|
|
157
|
+
)}
|
|
158
|
+
{!isActive && (
|
|
159
|
+
<button
|
|
160
|
+
className="host-picker-delete-btn"
|
|
161
|
+
onClick={(e) => {
|
|
162
|
+
e.stopPropagation();
|
|
163
|
+
setConfirmingDeleteId(host.hostId);
|
|
164
|
+
}}
|
|
165
|
+
aria-label={`Unpair ${displayName}`}
|
|
166
|
+
>
|
|
167
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
|
168
|
+
<path
|
|
169
|
+
d="M4 5.5V11C4 11.2761 4.22386 11.5 4.5 11.5H9.5C9.77614 11.5 10 11.2761 10 11V5.5M3 4H11M5.5 4V3C5.5 2.72386 5.72386 2.5 6 2.5H8C8.27614 2.5 8.5 2.72386 8.5 3V4M6.5 6.5V9.5M7.5 6.5V9.5"
|
|
170
|
+
stroke="currentColor"
|
|
171
|
+
strokeWidth="1.2"
|
|
172
|
+
strokeLinecap="round"
|
|
173
|
+
strokeLinejoin="round"
|
|
174
|
+
/>
|
|
175
|
+
</svg>
|
|
176
|
+
</button>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
})}
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
|
|
188
|
+
{!isLanMode && (<>
|
|
189
|
+
<div className="drawer-divider" />
|
|
190
|
+
<div className="drawer-section">
|
|
191
|
+
<button
|
|
192
|
+
className="btn btn-primary btn-full"
|
|
193
|
+
onClick={() => { navigate("/pair"); if (!isDesktop) close(); }}
|
|
194
|
+
>
|
|
195
|
+
Pair New Host
|
|
196
|
+
</button>
|
|
197
|
+
</div>
|
|
198
|
+
</>)}
|
|
199
|
+
|
|
200
|
+
<div className="drawer-footer">
|
|
201
|
+
{daemonVersion && (
|
|
202
|
+
<div className="drawer-version">
|
|
203
|
+
Palmier v{daemonVersion}
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
<div className="drawer-legal">
|
|
207
|
+
<a href="https://www.palmier.me/terms" target="_blank" rel="noopener noreferrer">Terms</a>
|
|
208
|
+
<span className="drawer-legal-sep">·</span>
|
|
209
|
+
<a href="https://www.palmier.me/privacy" target="_blank" rel="noopener noreferrer">Privacy</a>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
</>
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const deleteModal = confirmingDeleteId && createPortal(
|
|
216
|
+
<div className="confirm-modal-overlay" onClick={() => setConfirmingDeleteId(null)}>
|
|
217
|
+
<div className="confirm-modal" onClick={(e) => e.stopPropagation()}>
|
|
218
|
+
<h2 className="confirm-modal-title">Delete host?</h2>
|
|
219
|
+
<p className="confirm-modal-message">
|
|
220
|
+
"{pairedHosts.find((h) => h.hostId === confirmingDeleteId)?.name || confirmingDeleteId.slice(0, 8)}" will be unpaired from this device.
|
|
221
|
+
</p>
|
|
222
|
+
<div className="confirm-modal-actions">
|
|
223
|
+
<button
|
|
224
|
+
className="btn btn-secondary"
|
|
225
|
+
onClick={() => setConfirmingDeleteId(null)}
|
|
226
|
+
>
|
|
227
|
+
Cancel
|
|
228
|
+
</button>
|
|
229
|
+
<button
|
|
230
|
+
className="btn btn-danger"
|
|
231
|
+
onClick={() => {
|
|
232
|
+
removePairedHost(confirmingDeleteId);
|
|
233
|
+
setConfirmingDeleteId(null);
|
|
234
|
+
}}
|
|
235
|
+
>
|
|
236
|
+
Delete
|
|
237
|
+
</button>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
</div>,
|
|
241
|
+
document.body
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// Desktop: persistent inline sidebar
|
|
245
|
+
if (isDesktop) {
|
|
246
|
+
return (
|
|
247
|
+
<>
|
|
248
|
+
<div className="drawer-panel drawer-panel-desktop" ref={drawerRef}>
|
|
249
|
+
{drawerContent}
|
|
250
|
+
</div>
|
|
251
|
+
{deleteModal}
|
|
252
|
+
</>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Mobile: hamburger + slide-out drawer
|
|
257
|
+
return (
|
|
258
|
+
<>
|
|
259
|
+
<button
|
|
260
|
+
className="hamburger-btn"
|
|
261
|
+
onClick={openDrawer}
|
|
262
|
+
aria-label="Open menu"
|
|
263
|
+
>
|
|
264
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
|
265
|
+
<path d="M3 5H17M3 10H17M3 15H17" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
266
|
+
</svg>
|
|
267
|
+
</button>
|
|
268
|
+
|
|
269
|
+
{visible && createPortal(
|
|
270
|
+
<div
|
|
271
|
+
className={`drawer-overlay ${closing ? "drawer-overlay-closing" : ""}`}
|
|
272
|
+
onClick={close}
|
|
273
|
+
onAnimationEnd={handleAnimationEnd}
|
|
274
|
+
>
|
|
275
|
+
<div
|
|
276
|
+
className={`drawer-panel ${closing ? "drawer-panel-closing" : ""}`}
|
|
277
|
+
ref={drawerRef}
|
|
278
|
+
onClick={(e) => e.stopPropagation()}
|
|
279
|
+
>
|
|
280
|
+
{drawerContent}
|
|
281
|
+
</div>
|
|
282
|
+
</div>,
|
|
283
|
+
document.body
|
|
284
|
+
)}
|
|
285
|
+
|
|
286
|
+
{deleteModal}
|
|
287
|
+
</>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import Markdown from "react-markdown";
|
|
2
|
+
import remarkGfm from "remark-gfm";
|
|
3
|
+
import type { RequiredPermission } from "../types";
|
|
4
|
+
|
|
5
|
+
interface PlanDialogProps {
|
|
6
|
+
body: string;
|
|
7
|
+
permissions?: RequiredPermission[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function PlanDialog({ body, permissions }: PlanDialogProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="plan-dialog">
|
|
13
|
+
<h2>Task Execution Plan</h2>
|
|
14
|
+
<div className="plan-dialog-scroll">
|
|
15
|
+
{body ? (
|
|
16
|
+
<div className="plan-preview"><Markdown remarkPlugins={[remarkGfm]}>{body}</Markdown></div>
|
|
17
|
+
) : (
|
|
18
|
+
<p className="plan-empty">No execution plan generated for this task. Your task description will be used directly.</p>
|
|
19
|
+
)}
|
|
20
|
+
{permissions && permissions.length > 0 && (
|
|
21
|
+
<div className="permissions-section">
|
|
22
|
+
<h3>Granted Permissions</h3>
|
|
23
|
+
<ul className="permissions-list">
|
|
24
|
+
{permissions.map((p, i) => (
|
|
25
|
+
<li key={i} className="permission-item">
|
|
26
|
+
<span className="permission-tool">{p.name}</span>
|
|
27
|
+
<span className="permission-desc">{p.description}</span>
|
|
28
|
+
</li>
|
|
29
|
+
))}
|
|
30
|
+
</ul>
|
|
31
|
+
</div>
|
|
32
|
+
)}
|
|
33
|
+
</div>
|
|
34
|
+
<div className="plan-dialog-actions">
|
|
35
|
+
<button className="btn btn-secondary" onClick={() => history.back()}>
|
|
36
|
+
Back
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|