@snoglobe/helios 0.1.0 → 0.2.0

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 (247) hide show
  1. package/dist/acp/entry.d.ts +7 -0
  2. package/dist/acp/entry.d.ts.map +1 -0
  3. package/dist/acp/entry.js +46 -0
  4. package/dist/acp/entry.js.map +1 -0
  5. package/dist/acp/server.d.ts +57 -0
  6. package/dist/acp/server.d.ts.map +1 -0
  7. package/dist/acp/server.js +434 -0
  8. package/dist/acp/server.js.map +1 -0
  9. package/dist/acp/transport.d.ts +31 -0
  10. package/dist/acp/transport.d.ts.map +1 -0
  11. package/dist/acp/transport.js +116 -0
  12. package/dist/acp/transport.js.map +1 -0
  13. package/dist/acp/types.d.ts +213 -0
  14. package/dist/acp/types.d.ts.map +1 -0
  15. package/dist/acp/types.js +3 -0
  16. package/dist/acp/types.js.map +1 -0
  17. package/dist/app.d.ts +8 -1
  18. package/dist/app.d.ts.map +1 -1
  19. package/dist/app.js +51 -281
  20. package/dist/app.js.map +1 -1
  21. package/dist/bootstrap.d.ts +3 -0
  22. package/dist/bootstrap.d.ts.map +1 -0
  23. package/dist/bootstrap.js +26 -0
  24. package/dist/bootstrap.js.map +1 -0
  25. package/dist/cli/auth.d.ts +20 -0
  26. package/dist/cli/auth.d.ts.map +1 -0
  27. package/dist/cli/auth.js +62 -0
  28. package/dist/cli/auth.js.map +1 -0
  29. package/dist/cli/doctor.d.ts +9 -0
  30. package/dist/cli/doctor.d.ts.map +1 -0
  31. package/dist/cli/doctor.js +368 -0
  32. package/dist/cli/doctor.js.map +1 -0
  33. package/dist/cli/env.d.ts +13 -0
  34. package/dist/cli/env.d.ts.map +1 -0
  35. package/dist/cli/env.js +16 -0
  36. package/dist/cli/env.js.map +1 -0
  37. package/dist/cli/export.d.ts +11 -0
  38. package/dist/cli/export.d.ts.map +1 -0
  39. package/dist/cli/export.js +103 -0
  40. package/dist/cli/export.js.map +1 -0
  41. package/dist/cli/index.d.ts +22 -0
  42. package/dist/cli/index.d.ts.map +1 -0
  43. package/dist/cli/index.js +71 -0
  44. package/dist/cli/index.js.map +1 -0
  45. package/dist/cli/init-cmd.d.ts +11 -0
  46. package/dist/cli/init-cmd.d.ts.map +1 -0
  47. package/dist/cli/init-cmd.js +37 -0
  48. package/dist/cli/init-cmd.js.map +1 -0
  49. package/dist/cli/kill.d.ts +8 -0
  50. package/dist/cli/kill.d.ts.map +1 -0
  51. package/dist/cli/kill.js +46 -0
  52. package/dist/cli/kill.js.map +1 -0
  53. package/dist/cli/options.d.ts +18 -0
  54. package/dist/cli/options.d.ts.map +1 -0
  55. package/dist/cli/options.js +24 -0
  56. package/dist/cli/options.js.map +1 -0
  57. package/dist/cli/replay.d.ts +9 -0
  58. package/dist/cli/replay.d.ts.map +1 -0
  59. package/dist/cli/replay.js +65 -0
  60. package/dist/cli/replay.js.map +1 -0
  61. package/dist/cli/report.d.ts +11 -0
  62. package/dist/cli/report.d.ts.map +1 -0
  63. package/dist/cli/report.js +70 -0
  64. package/dist/cli/report.js.map +1 -0
  65. package/dist/cli/run.d.ts +17 -0
  66. package/dist/cli/run.d.ts.map +1 -0
  67. package/dist/cli/run.js +120 -0
  68. package/dist/cli/run.js.map +1 -0
  69. package/dist/cli/search.d.ts +9 -0
  70. package/dist/cli/search.d.ts.map +1 -0
  71. package/dist/cli/search.js +52 -0
  72. package/dist/cli/search.js.map +1 -0
  73. package/dist/cli/sessions.d.ts +8 -0
  74. package/dist/cli/sessions.d.ts.map +1 -0
  75. package/dist/cli/sessions.js +38 -0
  76. package/dist/cli/sessions.js.map +1 -0
  77. package/dist/cli/watch.d.ts +8 -0
  78. package/dist/cli/watch.d.ts.map +1 -0
  79. package/dist/cli/watch.js +81 -0
  80. package/dist/cli/watch.js.map +1 -0
  81. package/dist/config/project.d.ts +63 -0
  82. package/dist/config/project.d.ts.map +1 -0
  83. package/dist/config/project.js +112 -0
  84. package/dist/config/project.js.map +1 -0
  85. package/dist/core/orchestrator.d.ts +4 -2
  86. package/dist/core/orchestrator.d.ts.map +1 -1
  87. package/dist/core/orchestrator.js +8 -3
  88. package/dist/core/orchestrator.js.map +1 -1
  89. package/dist/core/task-poller.d.ts +43 -0
  90. package/dist/core/task-poller.d.ts.map +1 -0
  91. package/dist/core/task-poller.js +98 -0
  92. package/dist/core/task-poller.js.map +1 -0
  93. package/dist/experiments/branching.d.ts +38 -0
  94. package/dist/experiments/branching.d.ts.map +1 -0
  95. package/dist/experiments/branching.js +80 -0
  96. package/dist/experiments/branching.js.map +1 -0
  97. package/dist/hub/client.d.ts +70 -0
  98. package/dist/hub/client.d.ts.map +1 -0
  99. package/dist/hub/client.js +140 -0
  100. package/dist/hub/client.js.map +1 -0
  101. package/dist/hub/config.d.ts +9 -0
  102. package/dist/hub/config.d.ts.map +1 -0
  103. package/dist/hub/config.js +38 -0
  104. package/dist/hub/config.js.map +1 -0
  105. package/dist/index.d.ts +0 -1
  106. package/dist/index.js +25 -3
  107. package/dist/index.js.map +1 -1
  108. package/dist/init.d.ts +44 -0
  109. package/dist/init.d.ts.map +1 -0
  110. package/dist/init.js +384 -0
  111. package/dist/init.js.map +1 -0
  112. package/dist/memory/context-gate.d.ts.map +1 -1
  113. package/dist/memory/context-gate.js +1 -1
  114. package/dist/memory/context-gate.js.map +1 -1
  115. package/dist/memory/experiment-tracker.d.ts.map +1 -1
  116. package/dist/memory/experiment-tracker.js +2 -6
  117. package/dist/memory/experiment-tracker.js.map +1 -1
  118. package/dist/memory/memory-store.d.ts.map +1 -1
  119. package/dist/memory/memory-store.js +16 -20
  120. package/dist/memory/memory-store.js.map +1 -1
  121. package/dist/metrics/collector.d.ts.map +1 -1
  122. package/dist/metrics/collector.js +7 -7
  123. package/dist/metrics/collector.js.map +1 -1
  124. package/dist/metrics/resources.d.ts +44 -0
  125. package/dist/metrics/resources.d.ts.map +1 -0
  126. package/dist/metrics/resources.js +382 -0
  127. package/dist/metrics/resources.js.map +1 -0
  128. package/dist/metrics/store.d.ts +10 -2
  129. package/dist/metrics/store.d.ts.map +1 -1
  130. package/dist/metrics/store.js +91 -27
  131. package/dist/metrics/store.js.map +1 -1
  132. package/dist/notifications/index.d.ts +2 -0
  133. package/dist/notifications/index.d.ts.map +1 -0
  134. package/dist/notifications/index.js +2 -0
  135. package/dist/notifications/index.js.map +1 -0
  136. package/dist/notifications/notifier.d.ts +36 -0
  137. package/dist/notifications/notifier.d.ts.map +1 -0
  138. package/dist/notifications/notifier.js +102 -0
  139. package/dist/notifications/notifier.js.map +1 -0
  140. package/dist/paths.d.ts +3 -0
  141. package/dist/paths.d.ts.map +1 -0
  142. package/dist/paths.js +5 -0
  143. package/dist/paths.js.map +1 -0
  144. package/dist/providers/claude/provider.d.ts +7 -3
  145. package/dist/providers/claude/provider.d.ts.map +1 -1
  146. package/dist/providers/claude/provider.js +48 -8
  147. package/dist/providers/claude/provider.js.map +1 -1
  148. package/dist/providers/openai/callback-server.d.ts +6 -1
  149. package/dist/providers/openai/callback-server.d.ts.map +1 -1
  150. package/dist/providers/openai/callback-server.js +44 -20
  151. package/dist/providers/openai/callback-server.js.map +1 -1
  152. package/dist/providers/openai/oauth.d.ts +4 -0
  153. package/dist/providers/openai/oauth.d.ts.map +1 -1
  154. package/dist/providers/openai/oauth.js +34 -14
  155. package/dist/providers/openai/oauth.js.map +1 -1
  156. package/dist/providers/openai/provider.d.ts +6 -3
  157. package/dist/providers/openai/provider.d.ts.map +1 -1
  158. package/dist/providers/openai/provider.js +35 -9
  159. package/dist/providers/openai/provider.js.map +1 -1
  160. package/dist/providers/types.d.ts +9 -1
  161. package/dist/providers/types.d.ts.map +1 -1
  162. package/dist/providers/types.js.map +1 -1
  163. package/dist/remote/config.d.ts.map +1 -1
  164. package/dist/remote/config.js +4 -8
  165. package/dist/remote/config.js.map +1 -1
  166. package/dist/remote/connection-pool.d.ts.map +1 -1
  167. package/dist/remote/connection-pool.js +9 -5
  168. package/dist/remote/connection-pool.js.map +1 -1
  169. package/dist/store/database.d.ts.map +1 -1
  170. package/dist/store/database.js +2 -2
  171. package/dist/store/database.js.map +1 -1
  172. package/dist/store/migrations.d.ts.map +1 -1
  173. package/dist/store/migrations.js +27 -0
  174. package/dist/store/migrations.js.map +1 -1
  175. package/dist/store/preferences.js +2 -2
  176. package/dist/store/preferences.js.map +1 -1
  177. package/dist/store/session-store.d.ts +6 -0
  178. package/dist/store/session-store.d.ts.map +1 -1
  179. package/dist/store/session-store.js +20 -5
  180. package/dist/store/session-store.js.map +1 -1
  181. package/dist/tools/env-snapshot.d.ts +5 -0
  182. package/dist/tools/env-snapshot.d.ts.map +1 -0
  183. package/dist/tools/env-snapshot.js +174 -0
  184. package/dist/tools/env-snapshot.js.map +1 -0
  185. package/dist/tools/experiment-branch.d.ts +7 -0
  186. package/dist/tools/experiment-branch.d.ts.map +1 -0
  187. package/dist/tools/experiment-branch.js +167 -0
  188. package/dist/tools/experiment-branch.js.map +1 -0
  189. package/dist/tools/file-ops.d.ts.map +1 -1
  190. package/dist/tools/file-ops.js +1 -3
  191. package/dist/tools/file-ops.js.map +1 -1
  192. package/dist/tools/hub.d.ts +5 -0
  193. package/dist/tools/hub.d.ts.map +1 -0
  194. package/dist/tools/hub.js +298 -0
  195. package/dist/tools/hub.js.map +1 -0
  196. package/dist/tools/sweep.d.ts +6 -0
  197. package/dist/tools/sweep.d.ts.map +1 -0
  198. package/dist/tools/sweep.js +153 -0
  199. package/dist/tools/sweep.js.map +1 -0
  200. package/dist/tools/web-fetch.d.ts.map +1 -1
  201. package/dist/tools/web-fetch.js +2 -1
  202. package/dist/tools/web-fetch.js.map +1 -1
  203. package/dist/tools/writeup.d.ts +11 -0
  204. package/dist/tools/writeup.d.ts.map +1 -0
  205. package/dist/tools/writeup.js +102 -0
  206. package/dist/tools/writeup.js.map +1 -0
  207. package/dist/ui/commands.d.ts +36 -0
  208. package/dist/ui/commands.d.ts.map +1 -1
  209. package/dist/ui/commands.js +495 -1
  210. package/dist/ui/commands.js.map +1 -1
  211. package/dist/ui/components/input-bar.js.map +1 -1
  212. package/dist/ui/components/overlay-header.d.ts.map +1 -1
  213. package/dist/ui/components/overlay-header.js.map +1 -1
  214. package/dist/ui/components/status-bar.js +1 -1
  215. package/dist/ui/components/status-bar.js.map +1 -1
  216. package/dist/ui/format.d.ts +4 -0
  217. package/dist/ui/format.d.ts.map +1 -1
  218. package/dist/ui/format.js +16 -0
  219. package/dist/ui/format.js.map +1 -1
  220. package/dist/ui/layout.d.ts +7 -42
  221. package/dist/ui/layout.d.ts.map +1 -1
  222. package/dist/ui/layout.js +71 -543
  223. package/dist/ui/layout.js.map +1 -1
  224. package/dist/ui/overlays/metrics-overlay.js.map +1 -1
  225. package/dist/ui/overlays/task-overlay.d.ts +1 -1
  226. package/dist/ui/overlays/task-overlay.d.ts.map +1 -1
  227. package/dist/ui/overlays/task-overlay.js.map +1 -1
  228. package/dist/ui/panels/conversation.d.ts +1 -1
  229. package/dist/ui/panels/conversation.d.ts.map +1 -1
  230. package/dist/ui/panels/conversation.js +1 -2
  231. package/dist/ui/panels/conversation.js.map +1 -1
  232. package/dist/ui/panels/metrics-dashboard.d.ts.map +1 -1
  233. package/dist/ui/panels/metrics-dashboard.js.map +1 -1
  234. package/dist/ui/panels/sleep-panel.js.map +1 -1
  235. package/dist/ui/panels/sticky-notes.d.ts.map +1 -1
  236. package/dist/ui/panels/sticky-notes.js.map +1 -1
  237. package/dist/ui/panels/task-list.d.ts +4 -2
  238. package/dist/ui/panels/task-list.d.ts.map +1 -1
  239. package/dist/ui/panels/task-list.js +41 -9
  240. package/dist/ui/panels/task-list.js.map +1 -1
  241. package/dist/ui/theme.d.ts.map +1 -1
  242. package/dist/ui/theme.js.map +1 -1
  243. package/dist/ui/types.d.ts +22 -0
  244. package/dist/ui/types.d.ts.map +1 -0
  245. package/dist/ui/types.js +2 -0
  246. package/dist/ui/types.js.map +1 -0
  247. package/package.json +19 -6
package/dist/ui/layout.js CHANGED
@@ -5,22 +5,24 @@ import { useScreenSize } from "fullscreen-ink";
5
5
  import { ScrollView } from "ink-scroll-view";
6
6
  import { ConversationPanel } from "./panels/conversation.js";
7
7
  import { TaskListPanel } from "./panels/task-list.js";
8
- import { MetricsDashboard, sparkline } from "./panels/metrics-dashboard.js";
8
+ import { MetricsDashboard } from "./panels/metrics-dashboard.js";
9
9
  import { StatusBar } from "./components/status-bar.js";
10
10
  import { InputBar } from "./components/input-bar.js";
11
11
  import { C, G, HRule } from "./theme.js";
12
12
  import { KeyHintRule } from "./components/key-hint-rule.js";
13
13
  import { TaskOverlay } from "./overlays/task-overlay.js";
14
14
  import { MetricsOverlay } from "./overlays/metrics-overlay.js";
15
- import { formatMetricValue, formatError } from "./format.js";
16
- import { loadMachines, addMachine as addMachineConfig, removeMachine as removeMachineConfig, parseMachineSpec, } from "../remote/config.js";
17
15
  import { StickyNotesPanel } from "./panels/sticky-notes.js";
18
- import { savePreferences } from "../store/preferences.js";
19
16
  import { VERSION, checkForUpdate } from "../version.js";
17
+ import { handleSlashCommand } from "./commands.js";
18
+ import { pollTaskStatuses, handleFinishedTasks, buildMonitorMessage } from "../core/task-poller.js";
20
19
  let messageIdCounter = 0;
21
- export function Layout({ orchestrator, sleepManager, connectionPool, executor, metricStore, metricCollector, monitorManager, experimentTracker, memoryStore, stickyManager, mouseEmitter }) {
20
+ export function Layout({ runtime, mouseEmitter, headless, initialPrompt, initialAttachments }) {
21
+ const { orchestrator, sleepManager, connectionPool, executor, metricStore, metricCollector, monitorManager, experimentTracker, memoryStore, stickyManager, agentName, } = runtime;
22
22
  const { exit } = useApp();
23
23
  const [messages, setMessages] = useState([]);
24
+ const messagesRef = useRef(messages);
25
+ messagesRef.current = messages;
24
26
  const [isStreaming, setIsStreaming] = useState(false);
25
27
  const scrollRef = useRef(null);
26
28
  const [userScrolled, setUserScrolled] = useState(false);
@@ -28,6 +30,7 @@ export function Layout({ orchestrator, sleepManager, connectionPool, executor, m
28
30
  const [metricData, setMetricData] = useState(new Map());
29
31
  const [stickyNotes, setStickyNotes] = useState([]);
30
32
  const [activeOverlay, setActiveOverlay] = useState("none");
33
+ const [resourceData, setResourceData] = useState(new Map());
31
34
  const [updateAvailable, setUpdateAvailable] = useState(null);
32
35
  // Check for updates on mount (non-blocking)
33
36
  useEffect(() => {
@@ -38,72 +41,29 @@ export function Layout({ orchestrator, sleepManager, connectionPool, executor, m
38
41
  useEffect(() => {
39
42
  const poll = async () => {
40
43
  let didCollect = false;
41
- // Update task list from executor's background processes
42
44
  if (executor && connectionPool) {
43
- const procs = executor.getBackgroundProcesses();
44
- // Check all processes in parallel rather than sequentially
45
- const statuses = await Promise.all(procs.map(async (proc) => {
46
- try {
47
- const running = await executor.isRunning(proc.machineId, proc.pid);
48
- return { proc, running };
49
- }
50
- catch {
51
- return { proc, running: true }; // Transient error — assume still running
52
- }
53
- }));
54
- const finished = [];
55
- const updated = [];
56
- for (const { proc, running } of statuses) {
57
- const key = `${proc.machineId}:${proc.pid}`;
58
- const status = running ? "running" : "completed";
59
- if (!running)
60
- finished.push(key);
45
+ const { statuses, finished } = await pollTaskStatuses(executor);
46
+ // Build TaskInfo[] for UI state
47
+ const updated = statuses.map(({ proc, running }) => {
61
48
  const shortCmd = proc.command.length > 40
62
49
  ? proc.command.slice(0, 40) + "..."
63
50
  : proc.command;
64
- updated.push({
65
- id: key,
51
+ return {
52
+ id: `${proc.machineId}:${proc.pid}`,
66
53
  name: shortCmd,
67
- status,
54
+ status: running ? "running" : "completed",
68
55
  machineId: proc.machineId,
69
56
  pid: proc.pid,
70
57
  startedAt: proc.startedAt,
58
+ };
59
+ });
60
+ if (finished.length > 0) {
61
+ await handleFinishedTasks(finished, {
62
+ executor, connectionPool, metricCollector, metricStore,
63
+ experimentTracker, notifier: runtime.notifier,
71
64
  });
72
- }
73
- // Collect metrics before removing finished processes (so final data is captured)
74
- if (finished.length > 0 && metricCollector) {
75
- await metricCollector.collectAll().catch(() => { });
76
65
  didCollect = true;
77
66
  }
78
- for (const key of finished) {
79
- const [machineId, pidStr] = key.split(":");
80
- const pid = parseInt(pidStr, 10);
81
- // Fetch actual exit code before cleanup
82
- let exitCode = 0;
83
- try {
84
- const result = await connectionPool.exec(machineId, `wait ${pid} 2>/dev/null; echo $?`);
85
- const parsed = parseInt(result.stdout.trim(), 10);
86
- if (!isNaN(parsed))
87
- exitCode = parsed;
88
- }
89
- catch {
90
- // Can't determine exit code — default to 0
91
- }
92
- // Update experiment tracker with final metrics
93
- if (experimentTracker && metricStore) {
94
- const names = metricStore.getMetricNames(key);
95
- const metrics = {};
96
- for (const name of names) {
97
- const latest = metricStore.getLatest(key, name);
98
- if (latest)
99
- metrics[name] = latest.value;
100
- }
101
- experimentTracker.updateExperiment(machineId, pid, exitCode, Object.keys(metrics).length > 0 ? metrics : undefined);
102
- }
103
- // Clean up collector source so it stops tailing the dead process log
104
- metricCollector?.removeSource(key);
105
- executor.removeBackgroundProcess(key);
106
- }
107
67
  setTasks(updated);
108
68
  }
109
69
  // Collect metrics from all sources (skip if we already collected for finished tasks above)
@@ -111,21 +71,24 @@ export function Layout({ orchestrator, sleepManager, connectionPool, executor, m
111
71
  if (!didCollect) {
112
72
  await metricCollector.collectAll().catch(() => { });
113
73
  }
114
- // Build sparkline data from ALL known metrics (not just live processes)
115
- const newMetricData = new Map();
116
- const allNames = metricStore.getAllMetricNames();
117
- for (const name of allNames) {
118
- const series = metricStore.getSeriesAcrossTasks(name, 50);
119
- if (series.length > 0) {
120
- newMetricData.set(name, series.map((p) => p.value));
121
- }
122
- }
123
- setMetricData(newMetricData);
74
+ setMetricData(metricStore.getAllSeries(50));
75
+ }
76
+ // Collect resource usage from connected machines
77
+ if (runtime.resourceCollector) {
78
+ const res = await runtime.resourceCollector.collectAll().catch(() => new Map());
79
+ setResourceData(res);
124
80
  }
125
81
  };
126
- poll();
127
- const timer = setInterval(poll, 5000);
128
- return () => clearInterval(timer);
82
+ let timer = null;
83
+ let stopped = false;
84
+ const loop = async () => {
85
+ await poll();
86
+ if (!stopped)
87
+ timer = setTimeout(loop, 5000);
88
+ };
89
+ loop();
90
+ return () => { stopped = true; if (timer)
91
+ clearTimeout(timer); };
129
92
  }, [executor, connectionPool, metricCollector, metricStore]);
130
93
  // Auto-scroll to bottom when messages change, overlay closes, or user hasn't scrolled up
131
94
  useEffect(() => {
@@ -235,17 +198,19 @@ export function Layout({ orchestrator, sleepManager, connectionPool, executor, m
235
198
  const updateMessage = useCallback((id, updates) => {
236
199
  setMessages((prev) => prev.map((m) => (m.id === id ? { ...m, ...updates } : m)));
237
200
  }, []);
238
- const handleSubmit = useCallback(async (input) => {
201
+ const handleSubmit = useCallback(async (input, attachments) => {
239
202
  if (!input.trim())
240
203
  return;
241
204
  if (input.startsWith("/")) {
242
- if (input.startsWith("/writeup")) {
243
- await handleWriteup(orchestrator, messages, addMessage, updateMessage, setIsStreaming);
244
- return;
245
- }
246
- handleSlashCommand(input, {
247
- orchestrator, addMessage, setMessages, connectionPool,
248
- metricStore, metricCollector, memoryStore, stickyManager, setStickyNotes,
205
+ await handleSlashCommand(input, {
206
+ orchestrator, addMessage, updateMessage, setMessages, messages: messagesRef.current, setIsStreaming,
207
+ connectionPool, metricStore, metricCollector, memoryStore,
208
+ stickyManager, setStickyNotes, executor,
209
+ restoreMessages: (msgs) => msgs.map((m) => ({
210
+ id: ++messageIdCounter,
211
+ role: m.role,
212
+ content: m.content,
213
+ })),
249
214
  });
250
215
  return;
251
216
  }
@@ -262,7 +227,7 @@ export function Layout({ orchestrator, sleepManager, connectionPool, executor, m
262
227
  let assistantMsgId = null;
263
228
  // Map tool callId -> message id for attaching results
264
229
  const toolMsgIds = new Map();
265
- for await (const event of orchestrator.send(input)) {
230
+ for await (const event of orchestrator.send(input, attachments)) {
266
231
  // Feed events to experiment tracker for auto-populating /experiments/
267
232
  experimentTracker?.onEvent(event);
268
233
  if (event.type === "text" && event.delta) {
@@ -308,14 +273,17 @@ export function Layout({ orchestrator, sleepManager, connectionPool, executor, m
308
273
  setIsStreaming(false);
309
274
  }
310
275
  }, [orchestrator, sleepManager, addMessage, updateMessage, setMessages, connectionPool, metricStore]);
276
+ // Auto-submit initial prompt (from --prompt CLI flag)
277
+ const promptSent = useRef(false);
278
+ useEffect(() => {
279
+ if (initialPrompt && !promptSent.current) {
280
+ promptSent.current = true;
281
+ handleSubmit(initialPrompt, initialAttachments);
282
+ }
283
+ }, [initialPrompt, initialAttachments, handleSubmit]);
311
284
  // Monitor: auto-invoke model on tick
312
285
  const isStreamingRef = useRef(false);
313
286
  isStreamingRef.current = isStreaming;
314
- // Use refs for tasks/metricData so the monitor effect doesn't re-subscribe every poll
315
- const tasksRef = useRef(tasks);
316
- tasksRef.current = tasks;
317
- const metricDataRef = useRef(metricData);
318
- metricDataRef.current = metricData;
319
287
  const handleSubmitRef = useRef(handleSubmit);
320
288
  handleSubmitRef.current = handleSubmit;
321
289
  useEffect(() => {
@@ -324,29 +292,8 @@ export function Layout({ orchestrator, sleepManager, connectionPool, executor, m
324
292
  const onTick = (config) => {
325
293
  if (isStreamingRef.current)
326
294
  return;
327
- const elapsed = Date.now() - config.startedAt;
328
- const elapsedMin = Math.round(elapsed / 60_000);
329
- const intervalMin = Math.round(config.intervalMs / 60_000);
330
- const parts = [
331
- `[Monitor check — ${elapsedMin}m elapsed, interval ${intervalMin}m]`,
332
- `Goal: ${config.goal}`,
333
- ];
334
- const currentTasks = tasksRef.current;
335
- if (currentTasks.length > 0) {
336
- parts.push("Tasks:");
337
- for (const t of currentTasks) {
338
- parts.push(` ${t.status === "running" ? "◆" : "◇"} ${t.machineId}:${t.pid ?? "?"} ${t.status} — ${t.name}`);
339
- }
340
- }
341
- const currentMetrics = metricDataRef.current;
342
- if (currentMetrics.size > 0) {
343
- parts.push("Metrics:");
344
- for (const [name, values] of currentMetrics.entries()) {
345
- const latest = values[values.length - 1];
346
- parts.push(` ${name}: ${latest}`);
347
- }
348
- }
349
- handleSubmitRef.current(parts.join("\n"));
295
+ const message = buildMonitorMessage(config, executor, metricStore);
296
+ handleSubmitRef.current(message);
350
297
  };
351
298
  monitorManager.on("tick", onTick);
352
299
  return () => {
@@ -356,6 +303,13 @@ export function Layout({ orchestrator, sleepManager, connectionPool, executor, m
356
303
  // Sleep/wake: auto-resume model when a trigger fires
357
304
  const addMessageRef = useRef(addMessage);
358
305
  addMessageRef.current = addMessage;
306
+ // Route OpenAI OAuth URL display through the TUI instead of stderr
307
+ useEffect(() => {
308
+ runtime.openaiOAuth.onAuthUrl = (url) => {
309
+ addMessageRef.current("system", url);
310
+ };
311
+ return () => { runtime.openaiOAuth.onAuthUrl = null; };
312
+ }, [runtime.openaiOAuth]);
359
313
  useEffect(() => {
360
314
  const onWake = (_session, _reason, wakeMessage) => {
361
315
  if (isStreamingRef.current)
@@ -381,441 +335,15 @@ export function Layout({ orchestrator, sleepManager, connectionPool, executor, m
381
335
  return (_jsx(Box, { flexDirection: "column", height: height, width: width, children: _jsx(MetricsOverlay, { metricData: metricData, metricStore: metricStore, width: width, height: height, onClose: () => setActiveOverlay("none") }) }));
382
336
  }
383
337
  // ── Normal layout ─────────────────────────────────────────────
384
- return (_jsxs(Box, { flexDirection: "column", height: height, width: width, children: [_jsx(Box, { flexShrink: 0, children: _jsx(HeaderWithPanels, { width: width }) }), _jsxs(Box, { flexShrink: 0, flexDirection: "row", children: [_jsx(Box, { flexGrow: 1, flexBasis: 0, flexDirection: "column", paddingX: 1, children: _jsx(MetricsDashboard, { metricData: metricData, width: Math.floor((width - 1) / 2) - 2 }) }), _jsx(Box, { width: 1, flexDirection: "column", alignItems: "center", children: _jsx(Text, { color: C.primary, wrap: "truncate", children: Array.from({ length: panelHeight }, () => "│").join("\n") }) }), _jsx(Box, { flexGrow: 1, flexBasis: 0, flexDirection: "column", paddingX: 1, children: _jsx(TaskListPanel, { tasks: tasks, width: Math.floor((width - 1) / 2) - 2 }) })] }), _jsx(Box, { flexShrink: 0, children: _jsx(HRule, {}) }), _jsxs(Box, { flexGrow: 1, flexShrink: 1, flexDirection: "row", children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, children: messages.length === 0 ? (_jsxs(Box, { flexGrow: 1, alignItems: "center", justifyContent: "center", flexDirection: "column", children: [_jsx(Text, { color: C.primary, bold: true, children: G.brand }), _jsx(Text, { color: C.primary, bold: true, children: "H E L I O S" }), _jsx(Text, { color: C.dim, children: "autonomous ml research" }), _jsxs(Text, { color: C.dim, dimColor: true, children: ["v", VERSION] }), _jsx(Text, { color: C.dim, dimColor: true, children: "" }), _jsx(Text, { color: C.dim, dimColor: true, children: "/help for commands" }), updateAvailable && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: C.bright, children: ["update available: v", updateAvailable, " \u2014 npm i -g helios"] }) }))] })) : (_jsx(ScrollView, { ref: scrollRef, children: _jsx(ConversationPanel, { messages: messages, isStreaming: isStreaming }) })) }), stickyNotes.length > 0 && (_jsx(Box, { flexShrink: 0, children: _jsx(StickyNotesPanel, { notes: stickyNotes, width: Math.min(30, Math.floor(width * 0.25)) }) }))] }), _jsx(Box, { flexShrink: 0, children: _jsx(KeyHintRule, {}) }), _jsx(Box, { flexShrink: 0, children: _jsx(StatusBar, { orchestrator: orchestrator, sleepManager: sleepManager, monitorManager: monitorManager }) }), _jsx(Box, { flexShrink: 0, children: _jsx(InputBar, { onSubmit: handleSubmit, disabled: isStreaming, placeholder: isSleeping
338
+ return (_jsxs(Box, { flexDirection: "column", height: height, width: width, children: [_jsx(Box, { flexShrink: 0, children: headless && agentName ? (_jsx(HeadlessHeader, { agentName: agentName, width: width })) : (_jsx(HeaderWithPanels, { width: width })) }), _jsxs(Box, { flexShrink: 0, flexDirection: "row", children: [_jsx(Box, { flexGrow: 1, flexBasis: 0, flexDirection: "column", paddingX: 1, children: _jsx(MetricsDashboard, { metricData: metricData, width: Math.floor((width - 1) / 2) - 2 }) }), _jsx(Box, { width: 1, flexDirection: "column", alignItems: "center", children: _jsx(Text, { color: C.primary, wrap: "truncate", children: Array.from({ length: panelHeight }, () => "│").join("\n") }) }), _jsx(Box, { flexGrow: 1, flexBasis: 0, flexDirection: "column", paddingX: 1, children: _jsx(TaskListPanel, { tasks: tasks, resources: resourceData, width: Math.floor((width - 1) / 2) - 2 }) })] }), _jsx(Box, { flexShrink: 0, children: _jsx(HRule, {}) }), _jsxs(Box, { flexGrow: 1, flexShrink: 1, flexDirection: "row", children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, children: messages.length === 0 && !headless ? (_jsxs(Box, { flexGrow: 1, alignItems: "center", justifyContent: "center", flexDirection: "column", children: [_jsx(Text, { color: C.primary, bold: true, children: G.brand }), _jsx(Text, { color: C.primary, bold: true, children: "H E L I O S" }), _jsx(Text, { color: C.dim, children: "autonomous ml research" }), _jsxs(Text, { color: C.dim, dimColor: true, children: ["v", VERSION] }), _jsx(Text, { color: C.dim, dimColor: true, children: "" }), _jsx(Text, { color: C.dim, dimColor: true, children: "/help for commands" }), updateAvailable && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: C.bright, children: ["update available: v", updateAvailable, " \u2014 npm i -g helios"] }) }))] })) : (_jsx(ScrollView, { ref: scrollRef, children: _jsx(ConversationPanel, { messages: messages, isStreaming: isStreaming }) })) }), stickyNotes.length > 0 && (_jsx(Box, { flexShrink: 0, children: _jsx(StickyNotesPanel, { notes: stickyNotes, width: Math.min(30, Math.floor(width * 0.25)) }) }))] }), !headless && _jsx(Box, { flexShrink: 0, children: _jsx(KeyHintRule, {}) }), _jsx(Box, { flexShrink: 0, children: _jsx(StatusBar, { orchestrator: orchestrator, sleepManager: sleepManager, monitorManager: monitorManager }) }), !headless && (_jsx(Box, { flexShrink: 0, children: _jsx(InputBar, { onSubmit: handleSubmit, disabled: isStreaming, placeholder: isSleeping
385
339
  ? "type to wake agent..."
386
- : "send a message... (/help for commands)" }) })] }));
340
+ : "send a message... (/help for commands)" }) }))] }));
387
341
  }
388
- function handleSlashCommand(input, ctx) {
389
- const { orchestrator, addMessage, setMessages, connectionPool, metricStore, metricCollector, memoryStore, stickyManager, setStickyNotes } = ctx;
390
- const parts = input.slice(1).split(" ");
391
- const cmd = parts[0];
392
- const args = parts.slice(1);
393
- switch (cmd) {
394
- case "switch": {
395
- const provider = args[0];
396
- if (provider !== "claude" && provider !== "openai") {
397
- addMessage("system", "Usage: /switch <claude|openai>");
398
- return;
399
- }
400
- addMessage("system", `Switching to ${provider}...`);
401
- orchestrator.switchProvider(provider).then(() => addMessage("system", `Switched to ${provider}`), (err) => addMessage("error", `Failed to switch: ${formatError(err)}`));
402
- break;
403
- }
404
- case "model": {
405
- const modelId = args[0];
406
- if (!modelId) {
407
- addMessage("system", `Current model: ${orchestrator.currentModel ?? "default"}\nUsage: /model <model-id>`);
408
- return;
409
- }
410
- addMessage("system", `Setting model to ${modelId}...`);
411
- orchestrator.setModel(modelId).then(() => addMessage("system", `Model set to ${modelId}`), (err) => addMessage("error", `Failed to set model: ${formatError(err)}`));
412
- break;
413
- }
414
- case "reasoning": {
415
- const level = args[0];
416
- const validLevels = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
417
- if (!level || !validLevels.includes(level)) {
418
- const provider = orchestrator.currentProvider?.name;
419
- const hint = provider === "claude"
420
- ? "Claude: medium, high, max"
421
- : "OpenAI: none, minimal, low, medium, high, xhigh";
422
- addMessage("system", `Current reasoning effort: ${orchestrator.reasoningEffort ?? "medium"}\n${hint}\nUsage: /reasoning <level>`);
423
- return;
424
- }
425
- orchestrator.setReasoningEffort(level).then(() => addMessage("system", `Reasoning effort set to ${level}`), (err) => addMessage("error", `Failed: ${formatError(err)}`));
426
- break;
427
- }
428
- case "models": {
429
- addMessage("system", "Fetching available models...");
430
- orchestrator.fetchModels().then((models) => {
431
- const current = orchestrator.currentModel;
432
- const lines = models.map((m) => {
433
- const marker = m.id === current ? " ◆" : "";
434
- const desc = m.description ? ` — ${m.description}` : "";
435
- return ` ${m.id}${marker}${desc}`;
436
- });
437
- addMessage("system", `Available models:\n${lines.join("\n")}`);
438
- }, (err) => addMessage("error", `Failed to fetch models: ${formatError(err)}`));
439
- break;
440
- }
441
- case "claude-mode": {
442
- const mode = args[0];
443
- if (mode !== "cli" && mode !== "api") {
444
- const current = orchestrator.getProvider("claude")?.currentAuthMode;
445
- addMessage("system", `Current Claude mode: ${current === "cli" ? "cli (Agent SDK)" : "api (API key)"}\nUsage: /claude-mode <cli|api>`);
446
- break;
447
- }
448
- const claude = orchestrator.getProvider("claude");
449
- if (!claude) {
450
- addMessage("error", "Claude provider not registered");
451
- break;
452
- }
453
- claude.setPreferredAuthMode(mode);
454
- savePreferences({ claudeAuthMode: mode });
455
- // Re-authenticate to apply the new mode
456
- claude.authenticate().then(() => addMessage("system", `Claude mode set to ${mode === "cli" ? "cli (Agent SDK)" : "api (API key)"}`), (err) => addMessage("error", `Failed to switch Claude mode: ${formatError(err)}`));
457
- break;
458
- }
459
- case "machine":
460
- case "machines": {
461
- handleMachineCommand(args, addMessage, connectionPool);
462
- break;
463
- }
464
- case "resume": {
465
- handleResumeCommand(args, orchestrator, addMessage, setMessages);
466
- break;
467
- }
468
- case "metric":
469
- case "metrics": {
470
- if (!metricStore) {
471
- addMessage("error", "Metric store not available");
472
- break;
473
- }
474
- if (args[0] === "clear") {
475
- const deleted = metricStore.clear();
476
- metricCollector?.reset();
477
- addMessage("system", `Cleared ${deleted} metric points.`);
478
- }
479
- else if (args.length === 0) {
480
- // /metric with no args — list all known metric names
481
- const allNames = metricStore.getAllMetricNames();
482
- if (allNames.length === 0) {
483
- addMessage("system", "No metrics recorded yet.");
484
- }
485
- else {
486
- addMessage("system", `Known metrics:\n ${allNames.join(" ")}\n\nUsage: /metric <name1> [name2] ... | /metrics clear`);
487
- }
488
- }
489
- else {
490
- // /metric loss acc lr — show sparklines for named metrics
491
- const lines = [];
492
- for (const name of args) {
493
- const series = metricStore.getSeriesAcrossTasks(name, 50);
494
- if (series.length === 0) {
495
- lines.push(` ${name} (no data)`);
496
- continue;
497
- }
498
- const values = series.map((p) => p.value);
499
- const latest = values[values.length - 1];
500
- const min = Math.min(...values);
501
- const max = Math.max(...values);
502
- const spark = sparkline(values, 30);
503
- lines.push(` ${name} ${spark} ${formatMetricValue(latest)} (min ${formatMetricValue(min)} max ${formatMetricValue(max)})`);
504
- }
505
- addMessage("system", lines.join("\n"));
506
- }
507
- break;
508
- }
509
- case "help":
510
- addMessage("system", [
511
- "Commands:",
512
- " /switch <claude|openai> Switch model provider",
513
- " /claude-mode <cli|api> Switch Claude auth (cli=Agent SDK, api=API key)",
514
- " /model <model-id> Set model",
515
- " /models List available models",
516
- " /reasoning <level> Set reasoning effort",
517
- " /resume List recent sessions",
518
- " /resume <number> Resume a past session",
519
- " /metric [name1 name2 ...] Show metric sparklines",
520
- " /metrics clear Clear all metrics",
521
- " /writeup Generate experiment writeup",
522
- " /machine add <id> <user@host> Add remote machine",
523
- " /machine rm <id> Remove machine",
524
- " /machines List machines",
525
- " /status Show current state",
526
- " /clear Clear conversation",
527
- " /quit Exit Helios",
528
- "",
529
- "Keys:",
530
- " Tab Autocomplete command",
531
- " ↑↓ Navigate menu / history",
532
- " ←→ Move cursor",
533
- " Ctrl+T Task output overlay",
534
- " Ctrl+G Metrics overlay",
535
- " Escape Interrupt / close overlay",
536
- " Ctrl+A/E Start / end of line",
537
- " Ctrl+W Delete word backward",
538
- " Ctrl+U Clear line",
539
- " Ctrl+C Interrupt / Exit",
540
- ].join("\n"));
541
- break;
542
- case "status":
543
- addMessage("system", [
544
- `Provider: ${orchestrator.currentProvider?.displayName ?? "None"}`,
545
- `Model: ${orchestrator.currentModel ?? "default"}`,
546
- `Reasoning: ${orchestrator.reasoningEffort ?? "medium"}`,
547
- `State: ${orchestrator.currentState}`,
548
- `Cost: $${orchestrator.totalCostUsd.toFixed(4)}`,
549
- ].join("\n"));
550
- break;
551
- case "sticky": {
552
- if (!stickyManager || !setStickyNotes) {
553
- addMessage("system", "Sticky notes not available.");
554
- break;
555
- }
556
- const stickyText = args.join(" ").trim();
557
- if (!stickyText) {
558
- addMessage("system", "Usage: /sticky <text to pin>");
559
- break;
560
- }
561
- const note = stickyManager.add(stickyText);
562
- setStickyNotes(stickyManager.list());
563
- addMessage("system", `Pinned sticky #${note.num}: ${stickyText}`);
564
- break;
565
- }
566
- case "stickies": {
567
- if (!stickyManager || !setStickyNotes) {
568
- addMessage("system", "Sticky notes not available.");
569
- break;
570
- }
571
- if (args[0] === "rm" && args[1]) {
572
- const num = parseInt(args[1], 10);
573
- if (isNaN(num)) {
574
- addMessage("system", "Usage: /stickies rm <number>");
575
- break;
576
- }
577
- const removed = stickyManager.remove(num);
578
- setStickyNotes(stickyManager.list());
579
- addMessage("system", removed ? `Removed sticky #${num}` : `Sticky #${num} not found`);
580
- }
581
- else {
582
- const notes = stickyManager.list();
583
- if (notes.length === 0) {
584
- addMessage("system", "No sticky notes. Use /sticky <text> to add one.");
585
- }
586
- else {
587
- const listing = notes.map((n) => ` [${n.num}] ${n.text}`).join("\n");
588
- addMessage("system", `Sticky notes:\n${listing}`);
589
- }
590
- }
591
- break;
592
- }
593
- case "memory": {
594
- if (!memoryStore) {
595
- addMessage("system", "Memory system not initialized.");
596
- break;
597
- }
598
- const memPath = args[0] ?? "/";
599
- const tree = memoryStore.formatTree(memPath);
600
- addMessage("system", `Memory tree (${memPath}):\n${tree}`);
601
- break;
602
- }
603
- case "clear":
604
- setMessages([]);
605
- break;
606
- case "quit":
607
- case "exit":
608
- process.exit(0);
609
- default:
610
- addMessage("system", `Unknown command: /${cmd}. Try /help`);
611
- }
612
- }
613
- function handleMachineCommand(args, addMessage, connectionPool) {
614
- const subCmd = args[0];
615
- if (!subCmd || subCmd === "list") {
616
- const machines = loadMachines();
617
- if (machines.length === 0) {
618
- addMessage("system", "No machines configured.\nUsage: /machine add <id> <user@host[:port]> [--key <path>]");
619
- return;
620
- }
621
- const lines = machines.map((m) => {
622
- const status = connectionPool?.getStatus(m.id);
623
- let statusText = status?.connected ? "◆ connected" : "◇ disconnected";
624
- if (!status?.connected && status?.error) {
625
- statusText += ` — ${status.error}`;
626
- }
627
- return ` ${m.id} ${m.username}@${m.host}:${m.port} [${m.authMethod}] ${statusText}`;
628
- });
629
- addMessage("system", `Machines:\n${lines.join("\n")}`);
630
- return;
631
- }
632
- if (subCmd === "add") {
633
- const id = args[1];
634
- const spec = args[2];
635
- if (!id || !spec) {
636
- addMessage("system", "Usage: /machine add <id> <user@host[:port]> [--key <path>]");
637
- return;
638
- }
639
- const options = {};
640
- for (let i = 3; i < args.length; i++) {
641
- if (args[i] === "--key" && args[i + 1]) {
642
- options.key = args[++i];
643
- }
644
- else if (args[i] === "--auth" && args[i + 1]) {
645
- options.auth = args[++i];
646
- }
647
- }
648
- try {
649
- const machine = parseMachineSpec(id, spec, options);
650
- addMachineConfig(machine);
651
- connectionPool?.addMachine(machine);
652
- addMessage("system", `Added machine "${id}" (${machine.username}@${machine.host}:${machine.port}). Connecting...`);
653
- connectionPool?.connect(id).then(() => addMessage("system", `Machine "${id}" connected ◆`), (err) => addMessage("error", `Machine "${id}" added but connection failed: ${formatError(err)}\nThe agent can still try to connect later.`));
654
- }
655
- catch (err) {
656
- addMessage("error", `Failed to add machine: ${formatError(err)}`);
657
- }
658
- return;
659
- }
660
- if (subCmd === "rm" || subCmd === "remove") {
661
- const id = args[1];
662
- if (!id) {
663
- addMessage("system", "Usage: /machine rm <id>");
664
- return;
665
- }
666
- if (removeMachineConfig(id)) {
667
- connectionPool?.removeMachine(id);
668
- addMessage("system", `Removed machine "${id}"`);
669
- }
670
- else {
671
- addMessage("error", `Machine "${id}" not found`);
672
- }
673
- return;
674
- }
675
- addMessage("system", "Usage: /machine <add|rm|list>");
676
- }
677
- // Stash the last session listing so /resume <n> can look up by index
678
- let lastSessionListing = [];
679
- function handleResumeCommand(args, orchestrator, addMessage, setMessages) {
680
- const index = args[0] ? Number.parseInt(args[0], 10) : NaN;
681
- // --- /resume (no args) — list recent sessions ---
682
- if (Number.isNaN(index)) {
683
- const sessions = orchestrator.sessionStore.listSessionSummaries(20);
684
- if (sessions.length === 0) {
685
- addMessage("system", "No past sessions found.");
686
- return;
687
- }
688
- lastSessionListing = sessions;
689
- const lines = sessions.map((s, i) => {
690
- const date = new Date(s.lastActiveAt).toLocaleString();
691
- const provider = s.provider;
692
- const preview = s.firstUserMessage ?? "(no messages)";
693
- const msgs = `${s.messageCount} msg${s.messageCount !== 1 ? "s" : ""}`;
694
- return ` ${i + 1}. [${date}] ${provider} (${msgs})\n ${preview}`;
695
- });
696
- addMessage("system", `Recent sessions:\n${lines.join("\n")}\n\nUse /resume <number> to resume a session.`);
697
- return;
698
- }
699
- // --- /resume <number> — resume by index ---
700
- if (index < 1 || index > lastSessionListing.length) {
701
- addMessage("system", lastSessionListing.length === 0
702
- ? "Run /resume first to list sessions."
703
- : `Invalid index. Choose 1-${lastSessionListing.length}.`);
704
- return;
705
- }
706
- const target = lastSessionListing[index - 1];
707
- addMessage("system", `Resuming session from ${new Date(target.lastActiveAt).toLocaleString()}...`);
708
- // Load stored messages and restore them into the UI
709
- const storedMessages = orchestrator.sessionStore.getMessages(target.id, 500);
710
- // Build Message[] from stored messages, resetting the id counter
711
- const restored = storedMessages
712
- .filter((m) => m.role === "user" || m.role === "assistant")
713
- .map((m) => ({
714
- id: ++messageIdCounter,
715
- role: m.role,
716
- content: m.content,
717
- }));
718
- setMessages(restored);
719
- // Tell the orchestrator / provider to resume the session
720
- orchestrator.resumeSession(target.id).then(() => addMessage("system", `Session resumed (${target.provider}, ${storedMessages.length} messages loaded)`), (err) => addMessage("error", `Failed to resume session: ${formatError(err)}`));
721
- }
722
- const WRITEUP_SYSTEM_PROMPT = `You are a scientific writing assistant. You will receive the full transcript of an ML experiment session — including the researcher's goals, the agent's actions, tool calls, metric results, and conclusions.
723
-
724
- Your task: produce a clean, structured experiment writeup. Write it as a practitioner's report, not an academic paper. Be concise but thorough.
725
-
726
- ## Format
727
-
728
- # [Title — infer from the goal]
729
-
730
- ## Objective
731
- What was the researcher trying to achieve?
732
-
733
- ## Setup
734
- - Model architecture, dataset, hardware
735
- - Key hyperparameters and configuration
736
-
737
- ## Experiments
738
- For each distinct experiment/run:
739
- - What was tried and why
740
- - Key metrics (include actual numbers)
741
- - Whether it improved over the previous best
742
-
743
- ## Results
744
- - Best configuration found
745
- - Final metric values
746
- - Comparison to baseline / starting point
747
-
748
- ## Observations
749
- - What worked, what didn't
750
- - Surprising findings
751
- - Hypotheses about why certain changes helped/hurt
752
-
753
- ## Next Steps (if applicable)
754
- - Promising directions not yet explored
755
- - Known limitations
756
-
757
- Keep the writing direct and data-driven. Use actual metric values from the transcript. Do not invent data.`;
758
- async function handleWriteup(orchestrator, messages, addMessage, updateMessage, setIsStreaming) {
759
- if (messages.length === 0) {
760
- addMessage("system", "No conversation to write up.");
761
- return;
762
- }
763
- // Build a transcript from the conversation
764
- const transcript = messages
765
- .map((m) => {
766
- if (m.role === "user")
767
- return `[USER] ${m.content}`;
768
- if (m.role === "assistant")
769
- return `[ASSISTANT] ${m.content}`;
770
- if (m.role === "tool" && m.tool) {
771
- const result = m.tool.result ? `\nResult: ${m.tool.result}` : "";
772
- return `[TOOL: ${m.tool.name}] ${JSON.stringify(m.tool.args)}${result}`;
773
- }
774
- if (m.role === "system")
775
- return `[SYSTEM] ${m.content}`;
776
- if (m.role === "error")
777
- return `[ERROR] ${m.content}`;
778
- return "";
779
- })
780
- .filter(Boolean)
781
- .join("\n\n");
782
- addMessage("system", "Generating writeup...");
783
- setIsStreaming(true);
784
- try {
785
- // Get the active provider and create a one-shot session for the writeup
786
- const provider = orchestrator.currentProvider;
787
- if (!provider) {
788
- addMessage("error", "No active provider");
789
- return;
790
- }
791
- const writeupSession = await provider.createSession({
792
- systemPrompt: WRITEUP_SYSTEM_PROMPT,
793
- });
794
- try {
795
- let writeupText = "";
796
- let writeupMsgId = null;
797
- for await (const event of provider.send(writeupSession, `Here is the full experiment session transcript:\n\n${transcript}`, [])) {
798
- if (event.type === "text" && event.delta) {
799
- writeupText += event.delta;
800
- if (writeupMsgId === null) {
801
- writeupMsgId = addMessage("assistant", writeupText);
802
- }
803
- else {
804
- updateMessage(writeupMsgId, { content: writeupText });
805
- }
806
- }
807
- }
808
- }
809
- finally {
810
- await provider.closeSession(writeupSession).catch(() => { });
811
- }
812
- }
813
- catch (err) {
814
- addMessage("error", `Writeup failed: ${formatError(err)}`);
815
- }
816
- finally {
817
- setIsStreaming(false);
818
- }
342
+ /** Compact header for headless mode — shows agent name prominently. */
343
+ function HeadlessHeader({ agentName, width }) {
344
+ const label = ` ${G.brand} ${agentName} `;
345
+ const fill = Math.max(0, width - label.length);
346
+ return (_jsxs(Box, { children: [_jsx(Text, { color: C.bright, bold: true, children: label }), _jsx(Text, { color: C.primary, children: G.rule.repeat(fill) })] }));
819
347
  }
820
348
  /** Single header line: logo on the left, panel labels right-aligned in each half. */
821
349
  function HeaderWithPanels({ width }) {