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,343 +0,0 @@
1
- import { useState, useEffect, useRef } from "react";
2
- import { useNavigate } from "react-router-dom";
3
- import Markdown from "react-markdown";
4
- import remarkGfm from "remark-gfm";
5
- import remarkBreaks from "remark-breaks";
6
- import { getAgentLabel } from "../agentLabels";
7
- import { useFormatTime } from "../formatTime";
8
- import { useBackClose } from "../hooks/useBackClose";
9
- import type { ConversationMessage } from "../types";
10
-
11
- interface RunDetailViewProps {
12
- connected: boolean;
13
- hostId: string | null;
14
- request<T = unknown>(method: string, params?: unknown, opts?: { timeout?: number }): Promise<T>;
15
- subscribeEvents(hostId: string, callback: (msg: { subject: string; data: Uint8Array }) => void): () => void;
16
- taskId: string;
17
- runId: string;
18
- }
19
-
20
- export default function RunDetailView({ connected, hostId, request, subscribeEvents, taskId, runId }: RunDetailViewProps) {
21
- const navigate = useNavigate();
22
- const formatTime = useFormatTime();
23
- const [loading, setLoading] = useState(true);
24
- const [messages, setMessages] = useState<ConversationMessage[]>([]);
25
- const [runState, setRunState] = useState<string | undefined>();
26
- const [agent, setAgent] = useState<string | undefined>();
27
- const isTaskRunning = runState === "started" || runState === "monitoring";
28
- const isFollowupRunning = runState === "followup";
29
- const isAgentGenerating = runState === "started" || runState === "followup";
30
- const [reportDialog, setReportDialog] = useState<{ file: string; content?: string; data_url?: string } | null>(null);
31
- useBackClose(reportDialog !== null, () => setReportDialog(null));
32
- const [aborting, setAborting] = useState(false);
33
- const [followupText, setFollowupText] = useState("");
34
- const [sendingFollowup, setSendingFollowup] = useState(false);
35
- const threadRef = useRef<HTMLDivElement>(null);
36
- const followupInputRef = useRef<HTMLInputElement>(null);
37
- // Tracks which runId we've already scrolled-and-focused for, so new messages
38
- // during the session don't steal focus back to the input.
39
- const initialFocusForRunId = useRef<string | null>(null);
40
-
41
- // Resolve the "latest" sentinel to an actual run_id. `undefined` = still
42
- // resolving, `null` = task has no runs yet (empty state), otherwise the id
43
- // to load.
44
- const [resolvedRunId, setResolvedRunId] = useState<string | null | undefined>(
45
- runId === "latest" ? undefined : runId,
46
- );
47
- const isLatestEmpty = runId === "latest" && resolvedRunId === null;
48
-
49
- useEffect(() => {
50
- if (runId !== "latest") {
51
- setResolvedRunId(runId);
52
- return;
53
- }
54
- if (!connected) return;
55
- setResolvedRunId(undefined);
56
- request<{ entries?: Array<{ run_id: string }> }>("taskrun.list", { task_id: taskId, limit: 1 })
57
- .then((result) => setResolvedRunId(result.entries?.[0]?.run_id ?? null))
58
- .catch(() => setResolvedRunId(null));
59
- }, [runId, taskId, connected, request]);
60
-
61
- async function fetchData() {
62
- if (!resolvedRunId) return;
63
- try {
64
- const result = await request<{
65
- messages?: ConversationMessage[];
66
- start_time?: number;
67
- end_time?: number;
68
- running_state?: string;
69
- agent?: string;
70
- error?: string;
71
- }>("task.result", { id: taskId, run_id: resolvedRunId });
72
-
73
- if (result.error) {
74
- console.error("No result:", result.error);
75
- navigate(hostId ? `/hosts/${encodeURIComponent(hostId)}` : "/", { replace: true });
76
- return;
77
- }
78
- setMessages(result.messages ?? []);
79
- setRunState(result.running_state);
80
- setAgent(result.agent);
81
- } catch (err) {
82
- console.error("Failed to load result:", err);
83
- navigate(hostId ? `/hosts/${encodeURIComponent(hostId)}` : "/", { replace: true });
84
- } finally {
85
- setLoading(false);
86
- }
87
- }
88
-
89
- async function openReport(file: string) {
90
- if (!resolvedRunId) return;
91
- try {
92
- const result = await request<{ reports: Array<{ file: string; content?: string; data_url?: string }> }>(
93
- "task.reports", { id: taskId, run_id: resolvedRunId, report_files: [file] },
94
- );
95
- const report = result.reports?.[0];
96
- if (report?.data_url) {
97
- setReportDialog({ file, data_url: report.data_url });
98
- } else {
99
- setReportDialog({ file, content: report?.content ?? "Report not found." });
100
- }
101
- } catch {
102
- setReportDialog({ file, content: "Failed to load report." });
103
- }
104
- }
105
-
106
- // Initial load once resolvedRunId becomes a concrete id.
107
- useEffect(() => {
108
- if (!connected || !resolvedRunId) return;
109
- setLoading(true);
110
- fetchData();
111
- }, [connected, taskId, resolvedRunId]);
112
-
113
- // Live-update when the viewed task's state changes
114
- useEffect(() => {
115
- if (!connected || !hostId || !resolvedRunId) return;
116
- const unsubscribe = subscribeEvents(hostId, async (msg) => {
117
- try {
118
- const parsed = JSON.parse(new TextDecoder().decode(msg.data)) as { event_type?: string; run_id?: string };
119
- if (parsed.event_type !== "running-state" && parsed.event_type !== "result-updated") return;
120
- const eventTaskId = msg.subject.split(".").pop();
121
- if (eventTaskId !== taskId) return;
122
- if (parsed.event_type === "result-updated" && parsed.run_id && parsed.run_id !== resolvedRunId) return;
123
- fetchData();
124
- } catch { /* skip */ }
125
- });
126
- return unsubscribe;
127
- }, [connected, hostId, taskId, resolvedRunId, subscribeEvents, request]);
128
-
129
- // Auto-scroll to bottom when messages change
130
- useEffect(() => {
131
- if (threadRef.current) {
132
- threadRef.current.scrollTop = threadRef.current.scrollHeight;
133
- }
134
- }, [messages]);
135
-
136
- // On first load of a run, scroll the window to the bottom so the follow-up
137
- // input is visible. Deliberately not focusing the input — on mobile that
138
- // would pop the soft keyboard as soon as the run opens.
139
- useEffect(() => {
140
- if (loading || isLatestEmpty || !resolvedRunId) return;
141
- if (initialFocusForRunId.current === resolvedRunId) return;
142
- initialFocusForRunId.current = resolvedRunId;
143
- requestAnimationFrame(() => {
144
- if (threadRef.current) threadRef.current.scrollTop = threadRef.current.scrollHeight;
145
- window.scrollTo({ top: document.documentElement.scrollHeight, behavior: "auto" });
146
- });
147
- }, [loading, isLatestEmpty, resolvedRunId]);
148
-
149
- function typeLabel(type?: string): string | undefined {
150
- if (type === "input") return "User Input";
151
- if (type === "permission") return "Permission";
152
- if (type === "confirmation") return "Confirmation";
153
- return undefined;
154
- }
155
-
156
- function statusLabel(type?: string): string {
157
- if (type === "started") return "Task started";
158
- if (type === "finished") return "Task finished";
159
- if (type === "failed") return "Task failed";
160
- if (type === "error") return "Command failed";
161
- if (type === "aborted") return "Task aborted";
162
- if (type === "confirmation") return "Task confirmed";
163
- if (type === "stopped") return "Follow-up stopped";
164
- return type ?? "";
165
- }
166
-
167
- return (
168
- <div className="run-detail">
169
- <button className="run-detail-back" onClick={() => navigate(-1)}>
170
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
171
- <path d="M15 18l-6-6 6-6" />
172
- </svg>
173
- Back
174
- </button>
175
-
176
- {isLatestEmpty ? (
177
- <div className="empty-state">
178
- <p className="empty-state-text">No runs yet</p>
179
- <p className="empty-state-hint">This task hasn't been executed yet. Run it from the task menu or wait for its next trigger.</p>
180
- </div>
181
- ) : loading ? (
182
- <div style={{ display: "flex", flexDirection: "column", gap: "var(--space-sm)", padding: "var(--space-sm) 0" }}>
183
- <div className="skeleton-line" style={{ width: "40%" }} />
184
- <div className="skeleton-line" style={{ width: "55%" }} />
185
- <div className="skeleton-line" style={{ width: "100%", height: "8rem", marginTop: "var(--space-sm)" }} />
186
- </div>
187
- ) : (
188
- <>
189
- <div className="chat-thread" ref={threadRef}>
190
- {messages.map((msg, i) => {
191
- const isLastAssistant = isAgentGenerating && msg.role === "assistant" && !messages.slice(i + 1).some((m) => m.role === "assistant" || m.role === "user");
192
- if (msg.role === "status" && msg.type === "monitoring") return null;
193
- return msg.role === "status" ? (
194
- <div key={i} className={`chat-status${msg.type === "error" ? " chat-status--error" : ""}`}>
195
- <div>
196
- {statusLabel(msg.type)}
197
- {msg.time > 0 && <span className="chat-status-time">{formatTime(msg.time)}</span>}
198
- </div>
199
- {msg.content && <pre className="chat-status-detail">{msg.content}</pre>}
200
- </div>
201
- ) : (
202
- <div key={i} className={`chat-message chat-message--${msg.role}`}>
203
- {msg.role === "assistant" && agent && <div className="chat-message-agent">{getAgentLabel(agent)}</div>}
204
- <div className="chat-message-content">
205
- <Markdown remarkPlugins={[remarkGfm, remarkBreaks]}>{msg.content}</Markdown>
206
- {isLastAssistant && (
207
- <div className="chat-typing-indicator">
208
- <span /><span /><span />
209
- </div>
210
- )}
211
- </div>
212
- {msg.attachments && msg.attachments.length > 0 && (
213
- <div className="chat-message-attachments">
214
- {msg.attachments.map((file) => (
215
- <button key={file} className="chat-attachment-chip" onClick={() => openReport(file)}>
216
- {file}
217
- </button>
218
- ))}
219
- </div>
220
- )}
221
- <div className="chat-message-meta">
222
- {typeLabel(msg.type) && <span className="chat-message-type">{typeLabel(msg.type)}</span>}
223
- {msg.time > 0 && <span>{formatTime(msg.time)}</span>}
224
- </div>
225
- </div>
226
- );
227
- })}
228
- {isAgentGenerating && (() => { const nonStatus = messages.filter((m) => m.role !== "status"); return nonStatus.length === 0 || nonStatus[nonStatus.length - 1].role !== "assistant"; })() && (
229
- <div className="chat-message chat-message--assistant">
230
- {agent && <div className="chat-message-agent">{getAgentLabel(agent)}</div>}
231
- <div className="chat-typing-indicator">
232
- <span /><span /><span />
233
- </div>
234
- </div>
235
- )}
236
- {runState === "monitoring" && (
237
- <div className="chat-monitoring-indicator">
238
- <span className="chat-monitoring-dot" />
239
- Monitoring command output
240
- </div>
241
- )}
242
- </div>
243
- {isTaskRunning ? (
244
- <div className="chat-abort-bar">
245
- <button
246
- className="btn btn-secondary chat-abort-btn"
247
- disabled={aborting}
248
- onClick={async () => {
249
- if (!confirm("Abort this task?")) return;
250
- setAborting(true);
251
- try {
252
- await request("task.abort", { id: taskId });
253
- } catch (err) {
254
- console.error("Abort failed:", err);
255
- } finally {
256
- setAborting(false);
257
- }
258
- }}
259
- >
260
- {aborting ? "Aborting..." : "Abort Task"}
261
- </button>
262
- </div>
263
- ) : isFollowupRunning ? (
264
- <div className="chat-input-bar">
265
- <button
266
- className="btn btn-secondary chat-stop-btn"
267
- disabled={aborting}
268
- onClick={async () => {
269
- setAborting(true);
270
- try {
271
- await request("task.stop_followup", { id: taskId, run_id: resolvedRunId });
272
- } catch (err) {
273
- console.error("Stop failed:", err);
274
- } finally {
275
- setAborting(false);
276
- }
277
- }}
278
- >
279
- <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2" /></svg>
280
- </button>
281
- </div>
282
- ) : (
283
- <form
284
- className="chat-input-bar"
285
- onSubmit={async (e) => {
286
- e.preventDefault();
287
- const msg = followupText.trim();
288
- if (!msg || sendingFollowup) return;
289
- setSendingFollowup(true);
290
- try {
291
- await request("task.followup", { id: taskId, run_id: resolvedRunId, message: msg });
292
- setFollowupText("");
293
- } catch (err) {
294
- console.error("Follow-up failed:", err);
295
- } finally {
296
- setSendingFollowup(false);
297
- }
298
- }}
299
- >
300
- <input
301
- ref={followupInputRef}
302
- className="chat-input"
303
- type="text"
304
- placeholder="Follow-up message"
305
- value={followupText}
306
- onChange={(e) => setFollowupText(e.target.value)}
307
- disabled={sendingFollowup}
308
- />
309
- <button
310
- className="btn btn-primary chat-send-btn"
311
- type="submit"
312
- disabled={!followupText.trim() || sendingFollowup}
313
- >
314
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="22" y1="2" x2="11" y2="13" /><polygon points="22 2 15 22 11 13 2 9 22 2" /></svg>
315
- </button>
316
- </form>
317
- )}
318
- </>
319
- )}
320
- {reportDialog && (
321
- <div className="report-dialog-overlay" onClick={() => setReportDialog(null)}>
322
- <div className="report-dialog" onClick={(e) => e.stopPropagation()}>
323
- <div className="report-dialog-header">
324
- <span className="report-dialog-title">{reportDialog.file}</span>
325
- <button className="report-dialog-close" onClick={() => setReportDialog(null)} aria-label="Close">
326
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
327
- <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
328
- </svg>
329
- </button>
330
- </div>
331
- <div className="report-dialog-body">
332
- {reportDialog.data_url ? (
333
- <img src={reportDialog.data_url} alt={reportDialog.file} style={{ maxWidth: "100%", height: "auto" }} />
334
- ) : (
335
- <Markdown remarkPlugins={[remarkGfm, remarkBreaks]} components={{ a: ({ ...props }) => <a {...props} target="_blank" rel="noopener noreferrer" /> }}>{reportDialog.content ?? ""}</Markdown>
336
- )}
337
- </div>
338
- </div>
339
- </div>
340
- )}
341
- </div>
342
- );
343
- }
@@ -1,157 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import { useHostConnection } from "../contexts/HostConnectionContext";
3
- import { useHostStore } from "../contexts/HostStoreContext";
4
- import { setDraftMessage } from "../draftGuard";
5
- import type { AgentInfo } from "../types";
6
-
7
- interface SessionComposerProps {
8
- agents: AgentInfo[];
9
- hostPlatform?: string;
10
- onStarted(taskId: string, runId?: string): void;
11
- }
12
-
13
- function pickDefaultAgent(agents: AgentInfo[], preferred?: string): string {
14
- const keys = agents.map((a) => a.key);
15
- if (preferred && keys.includes(preferred)) return preferred;
16
- return agents[0]?.key ?? "";
17
- }
18
-
19
- export default function SessionComposer({ agents, hostPlatform, onStarted }: SessionComposerProps) {
20
- const { request, activeHost } = useHostConnection();
21
- const { setHostLastAgent } = useHostStore();
22
- const [prompt, setPrompt] = useState("");
23
- const [agent, setAgent] = useState(() => pickDefaultAgent(agents, activeHost.lastAgent));
24
- const [yoloMode, setYoloMode] = useState(false);
25
- const [running, setRunning] = useState(false);
26
- const [error, setError] = useState<string | null>(null);
27
-
28
- // Keep agent selection valid as the agent list arrives/changes.
29
- useEffect(() => {
30
- if (!agents.length) return;
31
- if (!agents.find((a) => a.key === agent)) {
32
- setAgent(pickDefaultAgent(agents, activeHost.lastAgent));
33
- }
34
- }, [agents, agent, activeHost.lastAgent]);
35
-
36
- // Draft guard: warns on navigation / reload when the input has content.
37
- useEffect(() => {
38
- const hasDraft = prompt.trim().length > 0;
39
- setDraftMessage(hasDraft ? "Your session draft will be lost. Continue?" : null);
40
- return () => setDraftMessage(null);
41
- }, [prompt]);
42
-
43
- const selectedAgent = agents.find((a) => a.key === agent);
44
- const agentSupportsYolo = !!selectedAgent?.supportsYolo;
45
-
46
- // Force-disable yolo when the selected agent doesn't support it.
47
- useEffect(() => {
48
- if (!agentSupportsYolo && yoloMode) setYoloMode(false);
49
- }, [agentSupportsYolo, yoloMode]);
50
-
51
- const canRun = !!prompt.trim() && !!agent && !running;
52
-
53
- function confirmYolo(): boolean {
54
- if (!yoloMode) return true;
55
- return confirm(
56
- "Yolo mode is enabled. The agent will auto-approve all tool calls \u2014 it can read, write, delete files, run arbitrary commands, and access the network without asking for permission.\n\nAre you sure you want to continue?"
57
- );
58
- }
59
-
60
- async function handleRun() {
61
- if (!canRun || !confirmYolo()) return;
62
- setRunning(true);
63
- setError(null);
64
- try {
65
- const result = await request<{ task_id?: string; run_id?: string; error?: string }>(
66
- "task.run_oneoff",
67
- {
68
- user_prompt: prompt,
69
- agent,
70
- yolo_mode: yoloMode,
71
- // Direct runs on Windows need a visible session so interactive tools
72
- // (browsers, GUI apps) can attach; background task-scheduler runs
73
- // would otherwise land in session 0 with no display.
74
- ...(hostPlatform === "win32" ? { foreground_mode: true } : {}),
75
- },
76
- );
77
- if (result.error) {
78
- setError(result.error);
79
- return;
80
- }
81
- setHostLastAgent(activeHost.hostId, agent);
82
- setPrompt("");
83
- setDraftMessage(null);
84
- if (result.task_id) onStarted(result.task_id, result.run_id);
85
- } catch (err) {
86
- setError(err instanceof Error ? err.message : String(err));
87
- } finally {
88
- setRunning(false);
89
- }
90
- }
91
-
92
- function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
93
- if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
94
- e.preventDefault();
95
- handleRun();
96
- }
97
- }
98
-
99
- return (
100
- <div className="session-composer">
101
- {error && <div className="form-error">{error}</div>}
102
- <textarea
103
- className="session-composer-textarea"
104
- value={prompt}
105
- onChange={(e) => setPrompt(e.target.value)}
106
- onKeyDown={handleKeyDown}
107
- placeholder="What can I do for you?"
108
- rows={3}
109
- disabled={running}
110
- />
111
- <div className="session-composer-controls">
112
- <div className="agent-picker-section-inline">
113
- <span className="agent-picker-label">Run with</span>
114
- <select
115
- className="form-select form-select-sm"
116
- value={agent}
117
- onChange={(e) => setAgent(e.target.value)}
118
- disabled={running || !agents.length}
119
- >
120
- {agents.map((a) => (
121
- <option key={a.key} value={a.key}>{a.label}</option>
122
- ))}
123
- </select>
124
- </div>
125
- {agentSupportsYolo && (
126
- <label className="session-composer-yolo">
127
- <input
128
- type="checkbox"
129
- checked={yoloMode}
130
- onChange={(e) => setYoloMode(e.target.checked)}
131
- disabled={running}
132
- />
133
- Yolo
134
- </label>
135
- )}
136
- <button
137
- className="btn btn-primary chat-send-btn"
138
- onClick={handleRun}
139
- disabled={!canRun}
140
- aria-label="Run session"
141
- title="Run session"
142
- >
143
- {running ? (
144
- <span className="btn-spinner" />
145
- ) : (
146
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="22" y1="2" x2="11" y2="13" /><polygon points="22 2 15 22 11 13 2 9 22 2" /></svg>
147
- )}
148
- </button>
149
- </div>
150
- {agentSupportsYolo && yoloMode && (
151
- <p className="command-help-text">
152
- The agent will auto-approve all tool calls without asking for permission.
153
- </p>
154
- )}
155
- </div>
156
- );
157
- }