@snoglobe/helios 0.1.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 (327) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +222 -0
  3. package/dist/app.d.ts +9 -0
  4. package/dist/app.d.ts.map +1 -0
  5. package/dist/app.js +289 -0
  6. package/dist/app.js.map +1 -0
  7. package/dist/core/agent-loop.d.ts +16 -0
  8. package/dist/core/agent-loop.d.ts.map +1 -0
  9. package/dist/core/agent-loop.js +64 -0
  10. package/dist/core/agent-loop.js.map +1 -0
  11. package/dist/core/monitor.d.ts +15 -0
  12. package/dist/core/monitor.d.ts.map +1 -0
  13. package/dist/core/monitor.js +40 -0
  14. package/dist/core/monitor.js.map +1 -0
  15. package/dist/core/orchestrator.d.ts +51 -0
  16. package/dist/core/orchestrator.d.ts.map +1 -0
  17. package/dist/core/orchestrator.js +235 -0
  18. package/dist/core/orchestrator.js.map +1 -0
  19. package/dist/core/state-machine.d.ts +19 -0
  20. package/dist/core/state-machine.d.ts.map +1 -0
  21. package/dist/core/state-machine.js +42 -0
  22. package/dist/core/state-machine.js.map +1 -0
  23. package/dist/core/stickies.d.ts +20 -0
  24. package/dist/core/stickies.d.ts.map +1 -0
  25. package/dist/core/stickies.js +41 -0
  26. package/dist/core/stickies.js.map +1 -0
  27. package/dist/index.d.ts +3 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +68 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/memory/context-gate.d.ts +49 -0
  32. package/dist/memory/context-gate.d.ts.map +1 -0
  33. package/dist/memory/context-gate.js +109 -0
  34. package/dist/memory/context-gate.js.map +1 -0
  35. package/dist/memory/experiment-tracker.d.ts +21 -0
  36. package/dist/memory/experiment-tracker.d.ts.map +1 -0
  37. package/dist/memory/experiment-tracker.js +117 -0
  38. package/dist/memory/experiment-tracker.js.map +1 -0
  39. package/dist/memory/memory-store.d.ts +37 -0
  40. package/dist/memory/memory-store.d.ts.map +1 -0
  41. package/dist/memory/memory-store.js +150 -0
  42. package/dist/memory/memory-store.js.map +1 -0
  43. package/dist/memory/token-estimator.d.ts +7 -0
  44. package/dist/memory/token-estimator.d.ts.map +1 -0
  45. package/dist/memory/token-estimator.js +30 -0
  46. package/dist/memory/token-estimator.js.map +1 -0
  47. package/dist/metrics/analyzer.d.ts +15 -0
  48. package/dist/metrics/analyzer.d.ts.map +1 -0
  49. package/dist/metrics/analyzer.js +67 -0
  50. package/dist/metrics/analyzer.js.map +1 -0
  51. package/dist/metrics/collector.d.ts +23 -0
  52. package/dist/metrics/collector.d.ts.map +1 -0
  53. package/dist/metrics/collector.js +60 -0
  54. package/dist/metrics/collector.js.map +1 -0
  55. package/dist/metrics/parser.d.ts +21 -0
  56. package/dist/metrics/parser.d.ts.map +1 -0
  57. package/dist/metrics/parser.js +43 -0
  58. package/dist/metrics/parser.js.map +1 -0
  59. package/dist/metrics/parsers/csv.d.ts +8 -0
  60. package/dist/metrics/parsers/csv.d.ts.map +1 -0
  61. package/dist/metrics/parsers/csv.js +37 -0
  62. package/dist/metrics/parsers/csv.js.map +1 -0
  63. package/dist/metrics/parsers/json.d.ts +8 -0
  64. package/dist/metrics/parsers/json.d.ts.map +1 -0
  65. package/dist/metrics/parsers/json.js +42 -0
  66. package/dist/metrics/parsers/json.js.map +1 -0
  67. package/dist/metrics/parsers/stdout.d.ts +3 -0
  68. package/dist/metrics/parsers/stdout.d.ts.map +1 -0
  69. package/dist/metrics/parsers/stdout.js +79 -0
  70. package/dist/metrics/parsers/stdout.js.map +1 -0
  71. package/dist/metrics/parsers/tensorboard.d.ts +16 -0
  72. package/dist/metrics/parsers/tensorboard.d.ts.map +1 -0
  73. package/dist/metrics/parsers/tensorboard.js +22 -0
  74. package/dist/metrics/parsers/tensorboard.js.map +1 -0
  75. package/dist/metrics/store.d.ts +33 -0
  76. package/dist/metrics/store.d.ts.map +1 -0
  77. package/dist/metrics/store.js +141 -0
  78. package/dist/metrics/store.js.map +1 -0
  79. package/dist/providers/auth/auth-manager.d.ts +22 -0
  80. package/dist/providers/auth/auth-manager.d.ts.map +1 -0
  81. package/dist/providers/auth/auth-manager.js +77 -0
  82. package/dist/providers/auth/auth-manager.js.map +1 -0
  83. package/dist/providers/auth/token-store.d.ts +14 -0
  84. package/dist/providers/auth/token-store.d.ts.map +1 -0
  85. package/dist/providers/auth/token-store.js +55 -0
  86. package/dist/providers/auth/token-store.js.map +1 -0
  87. package/dist/providers/claude/provider.d.ts +46 -0
  88. package/dist/providers/claude/provider.d.ts.map +1 -0
  89. package/dist/providers/claude/provider.js +606 -0
  90. package/dist/providers/claude/provider.js.map +1 -0
  91. package/dist/providers/openai/callback-server.d.ts +11 -0
  92. package/dist/providers/openai/callback-server.d.ts.map +1 -0
  93. package/dist/providers/openai/callback-server.js +58 -0
  94. package/dist/providers/openai/callback-server.js.map +1 -0
  95. package/dist/providers/openai/oauth.d.ts +16 -0
  96. package/dist/providers/openai/oauth.d.ts.map +1 -0
  97. package/dist/providers/openai/oauth.js +110 -0
  98. package/dist/providers/openai/oauth.js.map +1 -0
  99. package/dist/providers/openai/provider.d.ts +29 -0
  100. package/dist/providers/openai/provider.d.ts.map +1 -0
  101. package/dist/providers/openai/provider.js +363 -0
  102. package/dist/providers/openai/provider.js.map +1 -0
  103. package/dist/providers/retry.d.ts +6 -0
  104. package/dist/providers/retry.d.ts.map +1 -0
  105. package/dist/providers/retry.js +13 -0
  106. package/dist/providers/retry.js.map +1 -0
  107. package/dist/providers/sse.d.ts +6 -0
  108. package/dist/providers/sse.d.ts.map +1 -0
  109. package/dist/providers/sse.js +35 -0
  110. package/dist/providers/sse.js.map +1 -0
  111. package/dist/providers/types.d.ts +102 -0
  112. package/dist/providers/types.d.ts.map +1 -0
  113. package/dist/providers/types.js +11 -0
  114. package/dist/providers/types.js.map +1 -0
  115. package/dist/remote/config.d.ts +14 -0
  116. package/dist/remote/config.d.ts.map +1 -0
  117. package/dist/remote/config.js +81 -0
  118. package/dist/remote/config.js.map +1 -0
  119. package/dist/remote/connection-pool.d.ts +26 -0
  120. package/dist/remote/connection-pool.d.ts.map +1 -0
  121. package/dist/remote/connection-pool.js +268 -0
  122. package/dist/remote/connection-pool.js.map +1 -0
  123. package/dist/remote/executor.d.ts +23 -0
  124. package/dist/remote/executor.d.ts.map +1 -0
  125. package/dist/remote/executor.js +53 -0
  126. package/dist/remote/executor.js.map +1 -0
  127. package/dist/remote/file-sync.d.ts +19 -0
  128. package/dist/remote/file-sync.d.ts.map +1 -0
  129. package/dist/remote/file-sync.js +44 -0
  130. package/dist/remote/file-sync.js.map +1 -0
  131. package/dist/remote/types.d.ts +33 -0
  132. package/dist/remote/types.d.ts.map +1 -0
  133. package/dist/remote/types.js +2 -0
  134. package/dist/remote/types.js.map +1 -0
  135. package/dist/scheduler/sleep-manager.d.ts +33 -0
  136. package/dist/scheduler/sleep-manager.d.ts.map +1 -0
  137. package/dist/scheduler/sleep-manager.js +132 -0
  138. package/dist/scheduler/sleep-manager.js.map +1 -0
  139. package/dist/scheduler/ssh-batcher.d.ts +23 -0
  140. package/dist/scheduler/ssh-batcher.d.ts.map +1 -0
  141. package/dist/scheduler/ssh-batcher.js +98 -0
  142. package/dist/scheduler/ssh-batcher.js.map +1 -0
  143. package/dist/scheduler/state-store.d.ts +8 -0
  144. package/dist/scheduler/state-store.d.ts.map +1 -0
  145. package/dist/scheduler/state-store.js +48 -0
  146. package/dist/scheduler/state-store.js.map +1 -0
  147. package/dist/scheduler/trigger-scheduler.d.ts +24 -0
  148. package/dist/scheduler/trigger-scheduler.d.ts.map +1 -0
  149. package/dist/scheduler/trigger-scheduler.js +142 -0
  150. package/dist/scheduler/trigger-scheduler.js.map +1 -0
  151. package/dist/scheduler/triggers/file.d.ts +4 -0
  152. package/dist/scheduler/triggers/file.d.ts.map +1 -0
  153. package/dist/scheduler/triggers/file.js +39 -0
  154. package/dist/scheduler/triggers/file.js.map +1 -0
  155. package/dist/scheduler/triggers/metric.d.ts +4 -0
  156. package/dist/scheduler/triggers/metric.d.ts.map +1 -0
  157. package/dist/scheduler/triggers/metric.js +74 -0
  158. package/dist/scheduler/triggers/metric.js.map +1 -0
  159. package/dist/scheduler/triggers/process-exit.d.ts +4 -0
  160. package/dist/scheduler/triggers/process-exit.d.ts.map +1 -0
  161. package/dist/scheduler/triggers/process-exit.js +13 -0
  162. package/dist/scheduler/triggers/process-exit.js.map +1 -0
  163. package/dist/scheduler/triggers/resource.d.ts +4 -0
  164. package/dist/scheduler/triggers/resource.d.ts.map +1 -0
  165. package/dist/scheduler/triggers/resource.js +54 -0
  166. package/dist/scheduler/triggers/resource.js.map +1 -0
  167. package/dist/scheduler/triggers/timer.d.ts +3 -0
  168. package/dist/scheduler/triggers/timer.d.ts.map +1 -0
  169. package/dist/scheduler/triggers/timer.js +4 -0
  170. package/dist/scheduler/triggers/timer.js.map +1 -0
  171. package/dist/scheduler/triggers/types.d.ts +95 -0
  172. package/dist/scheduler/triggers/types.d.ts.map +1 -0
  173. package/dist/scheduler/triggers/types.js +11 -0
  174. package/dist/scheduler/triggers/types.js.map +1 -0
  175. package/dist/store/database.d.ts +5 -0
  176. package/dist/store/database.d.ts.map +1 -0
  177. package/dist/store/database.js +29 -0
  178. package/dist/store/database.js.map +1 -0
  179. package/dist/store/migrations.d.ts +3 -0
  180. package/dist/store/migrations.d.ts.map +1 -0
  181. package/dist/store/migrations.js +124 -0
  182. package/dist/store/migrations.js.map +1 -0
  183. package/dist/store/preferences.d.ts +7 -0
  184. package/dist/store/preferences.d.ts.map +1 -0
  185. package/dist/store/preferences.js +26 -0
  186. package/dist/store/preferences.js.map +1 -0
  187. package/dist/store/session-store.d.ts +29 -0
  188. package/dist/store/session-store.d.ts.map +1 -0
  189. package/dist/store/session-store.js +103 -0
  190. package/dist/store/session-store.js.map +1 -0
  191. package/dist/tools/clear-metrics.d.ts +5 -0
  192. package/dist/tools/clear-metrics.d.ts.map +1 -0
  193. package/dist/tools/clear-metrics.js +27 -0
  194. package/dist/tools/clear-metrics.js.map +1 -0
  195. package/dist/tools/compare-runs.d.ts +4 -0
  196. package/dist/tools/compare-runs.d.ts.map +1 -0
  197. package/dist/tools/compare-runs.js +77 -0
  198. package/dist/tools/compare-runs.js.map +1 -0
  199. package/dist/tools/consult.d.ts +3 -0
  200. package/dist/tools/consult.d.ts.map +1 -0
  201. package/dist/tools/consult.js +62 -0
  202. package/dist/tools/consult.js.map +1 -0
  203. package/dist/tools/file-ops.d.ts +6 -0
  204. package/dist/tools/file-ops.d.ts.map +1 -0
  205. package/dist/tools/file-ops.js +158 -0
  206. package/dist/tools/file-ops.js.map +1 -0
  207. package/dist/tools/kill-task.d.ts +6 -0
  208. package/dist/tools/kill-task.d.ts.map +1 -0
  209. package/dist/tools/kill-task.js +55 -0
  210. package/dist/tools/kill-task.js.map +1 -0
  211. package/dist/tools/list-machines.d.ts +4 -0
  212. package/dist/tools/list-machines.d.ts.map +1 -0
  213. package/dist/tools/list-machines.js +24 -0
  214. package/dist/tools/list-machines.js.map +1 -0
  215. package/dist/tools/memory-tools.d.ts +8 -0
  216. package/dist/tools/memory-tools.d.ts.map +1 -0
  217. package/dist/tools/memory-tools.js +122 -0
  218. package/dist/tools/memory-tools.js.map +1 -0
  219. package/dist/tools/metrics-query.d.ts +4 -0
  220. package/dist/tools/metrics-query.d.ts.map +1 -0
  221. package/dist/tools/metrics-query.js +69 -0
  222. package/dist/tools/metrics-query.js.map +1 -0
  223. package/dist/tools/monitor.d.ts +5 -0
  224. package/dist/tools/monitor.d.ts.map +1 -0
  225. package/dist/tools/monitor.js +64 -0
  226. package/dist/tools/monitor.js.map +1 -0
  227. package/dist/tools/remote-exec.d.ts +6 -0
  228. package/dist/tools/remote-exec.d.ts.map +1 -0
  229. package/dist/tools/remote-exec.js +98 -0
  230. package/dist/tools/remote-exec.js.map +1 -0
  231. package/dist/tools/remote-sync.d.ts +5 -0
  232. package/dist/tools/remote-sync.d.ts.map +1 -0
  233. package/dist/tools/remote-sync.js +57 -0
  234. package/dist/tools/remote-sync.js.map +1 -0
  235. package/dist/tools/show-metrics.d.ts +4 -0
  236. package/dist/tools/show-metrics.d.ts.map +1 -0
  237. package/dist/tools/show-metrics.js +69 -0
  238. package/dist/tools/show-metrics.js.map +1 -0
  239. package/dist/tools/sleep.d.ts +4 -0
  240. package/dist/tools/sleep.d.ts.map +1 -0
  241. package/dist/tools/sleep.js +116 -0
  242. package/dist/tools/sleep.js.map +1 -0
  243. package/dist/tools/task-output.d.ts +5 -0
  244. package/dist/tools/task-output.d.ts.map +1 -0
  245. package/dist/tools/task-output.js +51 -0
  246. package/dist/tools/task-output.js.map +1 -0
  247. package/dist/tools/web-fetch.d.ts +3 -0
  248. package/dist/tools/web-fetch.d.ts.map +1 -0
  249. package/dist/tools/web-fetch.js +112 -0
  250. package/dist/tools/web-fetch.js.map +1 -0
  251. package/dist/ui/commands.d.ts +7 -0
  252. package/dist/ui/commands.d.ts.map +1 -0
  253. package/dist/ui/commands.js +21 -0
  254. package/dist/ui/commands.js.map +1 -0
  255. package/dist/ui/components/gradient-edge.d.ts +7 -0
  256. package/dist/ui/components/gradient-edge.d.ts.map +1 -0
  257. package/dist/ui/components/gradient-edge.js +33 -0
  258. package/dist/ui/components/gradient-edge.js.map +1 -0
  259. package/dist/ui/components/input-bar.d.ts +8 -0
  260. package/dist/ui/components/input-bar.d.ts.map +1 -0
  261. package/dist/ui/components/input-bar.js +139 -0
  262. package/dist/ui/components/input-bar.js.map +1 -0
  263. package/dist/ui/components/key-hint-rule.d.ts +2 -0
  264. package/dist/ui/components/key-hint-rule.d.ts.map +1 -0
  265. package/dist/ui/components/key-hint-rule.js +19 -0
  266. package/dist/ui/components/key-hint-rule.js.map +1 -0
  267. package/dist/ui/components/overlay-header.d.ts +6 -0
  268. package/dist/ui/components/overlay-header.d.ts.map +1 -0
  269. package/dist/ui/components/overlay-header.js +10 -0
  270. package/dist/ui/components/overlay-header.js.map +1 -0
  271. package/dist/ui/components/status-bar.d.ts +11 -0
  272. package/dist/ui/components/status-bar.d.ts.map +1 -0
  273. package/dist/ui/components/status-bar.js +55 -0
  274. package/dist/ui/components/status-bar.js.map +1 -0
  275. package/dist/ui/format.d.ts +12 -0
  276. package/dist/ui/format.d.ts.map +1 -0
  277. package/dist/ui/format.js +36 -0
  278. package/dist/ui/format.js.map +1 -0
  279. package/dist/ui/layout.d.ts +48 -0
  280. package/dist/ui/layout.d.ts.map +1 -0
  281. package/dist/ui/layout.js +857 -0
  282. package/dist/ui/layout.js.map +1 -0
  283. package/dist/ui/markdown.d.ts +7 -0
  284. package/dist/ui/markdown.d.ts.map +1 -0
  285. package/dist/ui/markdown.js +67 -0
  286. package/dist/ui/markdown.js.map +1 -0
  287. package/dist/ui/mouse-filter.d.ts +21 -0
  288. package/dist/ui/mouse-filter.d.ts.map +1 -0
  289. package/dist/ui/mouse-filter.js +66 -0
  290. package/dist/ui/mouse-filter.js.map +1 -0
  291. package/dist/ui/overlays/metrics-overlay.d.ts +11 -0
  292. package/dist/ui/overlays/metrics-overlay.d.ts.map +1 -0
  293. package/dist/ui/overlays/metrics-overlay.js +86 -0
  294. package/dist/ui/overlays/metrics-overlay.js.map +1 -0
  295. package/dist/ui/overlays/task-overlay.d.ts +12 -0
  296. package/dist/ui/overlays/task-overlay.d.ts.map +1 -0
  297. package/dist/ui/overlays/task-overlay.js +77 -0
  298. package/dist/ui/overlays/task-overlay.js.map +1 -0
  299. package/dist/ui/panels/conversation.d.ts +8 -0
  300. package/dist/ui/panels/conversation.d.ts.map +1 -0
  301. package/dist/ui/panels/conversation.js +216 -0
  302. package/dist/ui/panels/conversation.js.map +1 -0
  303. package/dist/ui/panels/metrics-dashboard.d.ts +8 -0
  304. package/dist/ui/panels/metrics-dashboard.d.ts.map +1 -0
  305. package/dist/ui/panels/metrics-dashboard.js +56 -0
  306. package/dist/ui/panels/metrics-dashboard.js.map +1 -0
  307. package/dist/ui/panels/sleep-panel.d.ts +7 -0
  308. package/dist/ui/panels/sleep-panel.d.ts.map +1 -0
  309. package/dist/ui/panels/sleep-panel.js +48 -0
  310. package/dist/ui/panels/sleep-panel.js.map +1 -0
  311. package/dist/ui/panels/sticky-notes.d.ts +8 -0
  312. package/dist/ui/panels/sticky-notes.d.ts.map +1 -0
  313. package/dist/ui/panels/sticky-notes.js +49 -0
  314. package/dist/ui/panels/sticky-notes.js.map +1 -0
  315. package/dist/ui/panels/task-list.d.ts +8 -0
  316. package/dist/ui/panels/task-list.d.ts.map +1 -0
  317. package/dist/ui/panels/task-list.js +39 -0
  318. package/dist/ui/panels/task-list.js.map +1 -0
  319. package/dist/ui/theme.d.ts +25 -0
  320. package/dist/ui/theme.d.ts.map +1 -0
  321. package/dist/ui/theme.js +50 -0
  322. package/dist/ui/theme.js.map +1 -0
  323. package/dist/version.d.ts +9 -0
  324. package/dist/version.d.ts.map +1 -0
  325. package/dist/version.js +62 -0
  326. package/dist/version.js.map +1 -0
  327. package/package.json +59 -0
@@ -0,0 +1,857 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useCallback, useRef, useEffect } from "react";
3
+ import { Box, Text, useInput, useApp } from "ink";
4
+ import { useScreenSize } from "fullscreen-ink";
5
+ import { ScrollView } from "ink-scroll-view";
6
+ import { ConversationPanel } from "./panels/conversation.js";
7
+ import { TaskListPanel } from "./panels/task-list.js";
8
+ import { MetricsDashboard, sparkline } from "./panels/metrics-dashboard.js";
9
+ import { StatusBar } from "./components/status-bar.js";
10
+ import { InputBar } from "./components/input-bar.js";
11
+ import { C, G, HRule } from "./theme.js";
12
+ import { KeyHintRule } from "./components/key-hint-rule.js";
13
+ import { TaskOverlay } from "./overlays/task-overlay.js";
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
+ import { StickyNotesPanel } from "./panels/sticky-notes.js";
18
+ import { savePreferences } from "../store/preferences.js";
19
+ import { VERSION, checkForUpdate } from "../version.js";
20
+ let messageIdCounter = 0;
21
+ export function Layout({ orchestrator, sleepManager, connectionPool, executor, metricStore, metricCollector, monitorManager, experimentTracker, memoryStore, stickyManager, mouseEmitter }) {
22
+ const { exit } = useApp();
23
+ const [messages, setMessages] = useState([]);
24
+ const [isStreaming, setIsStreaming] = useState(false);
25
+ const scrollRef = useRef(null);
26
+ const [userScrolled, setUserScrolled] = useState(false);
27
+ const [tasks, setTasks] = useState([]);
28
+ const [metricData, setMetricData] = useState(new Map());
29
+ const [stickyNotes, setStickyNotes] = useState([]);
30
+ const [activeOverlay, setActiveOverlay] = useState("none");
31
+ const [updateAvailable, setUpdateAvailable] = useState(null);
32
+ // Check for updates on mount (non-blocking)
33
+ useEffect(() => {
34
+ checkForUpdate().then((v) => { if (v)
35
+ setUpdateAvailable(v); }).catch(() => { });
36
+ }, []);
37
+ // Poll tasks and metrics every 5 seconds
38
+ useEffect(() => {
39
+ const poll = async () => {
40
+ let didCollect = false;
41
+ // Update task list from executor's background processes
42
+ 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);
61
+ const shortCmd = proc.command.length > 40
62
+ ? proc.command.slice(0, 40) + "..."
63
+ : proc.command;
64
+ updated.push({
65
+ id: key,
66
+ name: shortCmd,
67
+ status,
68
+ machineId: proc.machineId,
69
+ pid: proc.pid,
70
+ startedAt: proc.startedAt,
71
+ });
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
+ didCollect = true;
77
+ }
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
+ setTasks(updated);
108
+ }
109
+ // Collect metrics from all sources (skip if we already collected for finished tasks above)
110
+ if (metricCollector && metricStore) {
111
+ if (!didCollect) {
112
+ await metricCollector.collectAll().catch(() => { });
113
+ }
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);
124
+ }
125
+ };
126
+ poll();
127
+ const timer = setInterval(poll, 5000);
128
+ return () => clearInterval(timer);
129
+ }, [executor, connectionPool, metricCollector, metricStore]);
130
+ // Auto-scroll to bottom when messages change, overlay closes, or user hasn't scrolled up
131
+ useEffect(() => {
132
+ if (!userScrolled) {
133
+ scrollRef.current?.scrollToBottom();
134
+ }
135
+ }, [messages, userScrolled, activeOverlay]);
136
+ // Re-snap to bottom when streaming starts, and keep scrolling during streaming
137
+ useEffect(() => {
138
+ if (isStreaming) {
139
+ setUserScrolled(false);
140
+ // During streaming, content changes faster than React state updates trigger effects.
141
+ // Poll scrollToBottom on a short interval to keep up.
142
+ const timer = setInterval(() => {
143
+ scrollRef.current?.scrollToBottom();
144
+ }, 100);
145
+ return () => clearInterval(timer);
146
+ }
147
+ }, [isStreaming]);
148
+ // Clamped scroll helper — ink-scroll-view's scrollBy has a bug where
149
+ // it clamps to contentHeight instead of contentHeight - viewportHeight,
150
+ // allowing you to scroll past the bottom into empty space.
151
+ const clampedScrollBy = useCallback((delta) => {
152
+ const sv = scrollRef.current;
153
+ if (!sv)
154
+ return;
155
+ const target = Math.max(0, Math.min(sv.getScrollOffset() + delta, sv.getBottomOffset()));
156
+ sv.scrollTo(target);
157
+ return target >= sv.getBottomOffset();
158
+ }, []);
159
+ // Enable SGR mouse reporting and handle scroll via mouseEmitter
160
+ useEffect(() => {
161
+ process.stdout.write("\x1b[?1000h\x1b[?1006h");
162
+ return () => { process.stdout.write("\x1b[?1006l\x1b[?1000l"); };
163
+ }, []);
164
+ useEffect(() => {
165
+ if (!mouseEmitter)
166
+ return;
167
+ const handler = (evt) => {
168
+ if (evt.type === "scroll_up") {
169
+ clampedScrollBy(-3);
170
+ setUserScrolled(true);
171
+ }
172
+ else if (evt.type === "scroll_down") {
173
+ const atBottom = clampedScrollBy(3);
174
+ if (atBottom)
175
+ setUserScrolled(false);
176
+ }
177
+ };
178
+ mouseEmitter.on("mouse", handler);
179
+ return () => { mouseEmitter.removeListener("mouse", handler); };
180
+ }, [mouseEmitter, clampedScrollBy]);
181
+ useInput((input, key) => {
182
+ // Toggle overlays — always available
183
+ if (key.ctrl && input === "t") {
184
+ setActiveOverlay((prev) => prev === "tasks" ? "none" : "tasks");
185
+ return;
186
+ }
187
+ if (key.ctrl && input === "g") {
188
+ setActiveOverlay((prev) => prev === "metrics" ? "none" : "metrics");
189
+ return;
190
+ }
191
+ // Esc: close overlay first, then interrupt stream
192
+ if (key.escape) {
193
+ if (activeOverlay !== "none") {
194
+ setActiveOverlay("none");
195
+ return;
196
+ }
197
+ if (isStreaming) {
198
+ orchestrator.interrupt();
199
+ setIsStreaming(false);
200
+ return;
201
+ }
202
+ }
203
+ if (key.ctrl && input === "c") {
204
+ if (activeOverlay !== "none") {
205
+ setActiveOverlay("none");
206
+ return;
207
+ }
208
+ if (isStreaming) {
209
+ orchestrator.interrupt();
210
+ setIsStreaming(false);
211
+ }
212
+ else {
213
+ exit();
214
+ }
215
+ return;
216
+ }
217
+ // Don't process scroll keys when overlay is active
218
+ if (activeOverlay !== "none")
219
+ return;
220
+ if (key.pageUp) {
221
+ clampedScrollBy(-10);
222
+ setUserScrolled(true);
223
+ }
224
+ if (key.pageDown) {
225
+ const atBottom = clampedScrollBy(10);
226
+ if (atBottom)
227
+ setUserScrolled(false);
228
+ }
229
+ });
230
+ const addMessage = useCallback((role, content, tool) => {
231
+ const id = ++messageIdCounter;
232
+ setMessages((prev) => [...prev, { id, role, content, tool }]);
233
+ return id;
234
+ }, []);
235
+ const updateMessage = useCallback((id, updates) => {
236
+ setMessages((prev) => prev.map((m) => (m.id === id ? { ...m, ...updates } : m)));
237
+ }, []);
238
+ const handleSubmit = useCallback(async (input) => {
239
+ if (!input.trim())
240
+ return;
241
+ 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,
249
+ });
250
+ return;
251
+ }
252
+ if (sleepManager.isSleeping) {
253
+ addMessage("user", input);
254
+ addMessage("system", "Waking agent...");
255
+ sleepManager.manualWake(input);
256
+ return;
257
+ }
258
+ addMessage("user", input);
259
+ setIsStreaming(true);
260
+ try {
261
+ let assistantText = "";
262
+ let assistantMsgId = null;
263
+ // Map tool callId -> message id for attaching results
264
+ const toolMsgIds = new Map();
265
+ for await (const event of orchestrator.send(input)) {
266
+ // Feed events to experiment tracker for auto-populating /experiments/
267
+ experimentTracker?.onEvent(event);
268
+ if (event.type === "text" && event.delta) {
269
+ assistantText += event.delta;
270
+ if (assistantMsgId === null) {
271
+ assistantMsgId = addMessage("assistant", assistantText);
272
+ }
273
+ else {
274
+ updateMessage(assistantMsgId, { content: assistantText });
275
+ }
276
+ }
277
+ if (event.type === "tool_call") {
278
+ const toolData = {
279
+ callId: event.id,
280
+ name: event.name,
281
+ args: event.args,
282
+ };
283
+ const msgId = addMessage("tool", "", toolData);
284
+ toolMsgIds.set(event.id, msgId);
285
+ assistantText = "";
286
+ assistantMsgId = null;
287
+ }
288
+ if (event.type === "tool_result") {
289
+ const msgId = toolMsgIds.get(event.callId);
290
+ if (msgId !== undefined) {
291
+ setMessages((prev) => prev.map((m) => m.id === msgId && m.tool
292
+ ? { ...m, tool: { ...m.tool, result: event.result, isError: event.isError } }
293
+ : m));
294
+ }
295
+ if (event.isError) {
296
+ addMessage("error", event.result);
297
+ }
298
+ }
299
+ if (event.type === "error") {
300
+ addMessage("error", event.error.message);
301
+ }
302
+ }
303
+ }
304
+ catch (err) {
305
+ addMessage("error", err instanceof Error ? err.message : "Unknown error");
306
+ }
307
+ finally {
308
+ setIsStreaming(false);
309
+ }
310
+ }, [orchestrator, sleepManager, addMessage, updateMessage, setMessages, connectionPool, metricStore]);
311
+ // Monitor: auto-invoke model on tick
312
+ const isStreamingRef = useRef(false);
313
+ 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
+ const handleSubmitRef = useRef(handleSubmit);
320
+ handleSubmitRef.current = handleSubmit;
321
+ useEffect(() => {
322
+ if (!monitorManager)
323
+ return;
324
+ const onTick = (config) => {
325
+ if (isStreamingRef.current)
326
+ 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"));
350
+ };
351
+ monitorManager.on("tick", onTick);
352
+ return () => {
353
+ monitorManager.removeListener("tick", onTick);
354
+ };
355
+ }, [monitorManager]);
356
+ // Sleep/wake: auto-resume model when a trigger fires
357
+ const addMessageRef = useRef(addMessage);
358
+ addMessageRef.current = addMessage;
359
+ useEffect(() => {
360
+ const onWake = (_session, _reason, wakeMessage) => {
361
+ if (isStreamingRef.current)
362
+ return;
363
+ addMessageRef.current("system", "Agent waking up — trigger fired");
364
+ handleSubmitRef.current(wakeMessage);
365
+ };
366
+ sleepManager.on("wake", onWake);
367
+ return () => {
368
+ sleepManager.removeListener("wake", onWake);
369
+ };
370
+ }, [sleepManager]);
371
+ const isSleeping = sleepManager.isSleeping;
372
+ const metricsRows = metricData.size > 0 ? metricData.size : 1;
373
+ const tasksRows = tasks.length > 0 ? Math.min(tasks.length, 5) : 1;
374
+ const panelHeight = Math.max(metricsRows, tasksRows);
375
+ const { height, width } = useScreenSize();
376
+ // ── Fullscreen overlays ───────────────────────────────────────
377
+ if (activeOverlay === "tasks") {
378
+ return (_jsx(Box, { flexDirection: "column", height: height, width: width, children: _jsx(TaskOverlay, { tasks: tasks, executor: executor, width: width, height: height, onClose: () => setActiveOverlay("none") }) }));
379
+ }
380
+ if (activeOverlay === "metrics") {
381
+ return (_jsx(Box, { flexDirection: "column", height: height, width: width, children: _jsx(MetricsOverlay, { metricData: metricData, metricStore: metricStore, width: width, height: height, onClose: () => setActiveOverlay("none") }) }));
382
+ }
383
+ // ── 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
385
+ ? "type to wake agent..."
386
+ : "send a message... (/help for commands)" }) })] }));
387
+ }
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
+ }
819
+ }
820
+ /** Single header line: logo on the left, panel labels right-aligned in each half. */
821
+ function HeaderWithPanels({ width }) {
822
+ const logo = ` ▓▒░ ${G.brand} HELIOS ░▒▓ `;
823
+ const ver = `${VERSION} `;
824
+ const metricsLabel = ` ⣤⣸⣿ METRICS `;
825
+ const tasksLabel = ` ⊳ TASKS `;
826
+ const half = Math.floor(width / 2);
827
+ const leftFill = Math.max(0, half - logo.length - ver.length - metricsLabel.length - 1);
828
+ const rightFill = Math.max(0, width - half - tasksLabel.length - 1);
829
+ return (_jsxs(Box, { children: [_jsx(ShimmerLogo, { text: logo }), _jsx(Text, { color: C.dim, children: ver }), _jsx(Text, { color: C.primary, children: G.rule.repeat(leftFill) }), _jsx(Text, { color: C.primary, children: metricsLabel }), _jsx(Text, { color: C.primary, children: G.rule }), _jsx(Text, { color: C.primary, children: G.rule.repeat(rightFill) }), _jsx(Text, { color: C.primary, children: tasksLabel }), _jsx(Text, { color: C.primary, children: G.rule })] }));
830
+ }
831
+ const SHIMMER_INTERVAL = 80;
832
+ const SHIMMER_PAUSE = 20; // extra frames of pause after sweep
833
+ function ShimmerLogo({ text }) {
834
+ const [frame, setFrame] = useState(0);
835
+ const len = text.length;
836
+ const cycleLen = len + 6 + SHIMMER_PAUSE; // 6 = shimmer tail width
837
+ useEffect(() => {
838
+ const timer = setInterval(() => setFrame((f) => (f + 1) % cycleLen), SHIMMER_INTERVAL);
839
+ return () => clearInterval(timer);
840
+ }, [cycleLen]);
841
+ const shimmerPos = frame - 3; // center of the bright spot
842
+ // Group consecutive chars by color into segments for fewer <Text> nodes
843
+ const segments = [];
844
+ for (let i = 0; i < len; i++) {
845
+ const dist = Math.abs(i - shimmerPos);
846
+ const color = dist <= 1 ? C.bright : C.primary;
847
+ const prev = segments[segments.length - 1];
848
+ if (prev && prev.color === color) {
849
+ prev.chars += text[i];
850
+ }
851
+ else {
852
+ segments.push({ color, chars: text[i] });
853
+ }
854
+ }
855
+ return (_jsx(Text, { children: segments.map((seg, i) => (_jsx(Text, { color: seg.color, bold: true, children: seg.chars }, i))) }));
856
+ }
857
+ //# sourceMappingURL=layout.js.map