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,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
- }