loopsy 1.0.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 (262) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +425 -0
  3. package/dist/cli/commands/connect.d.ts +2 -0
  4. package/dist/cli/commands/connect.d.ts.map +1 -0
  5. package/dist/cli/commands/connect.js +120 -0
  6. package/dist/cli/commands/connect.js.map +1 -0
  7. package/dist/cli/commands/context.d.ts +2 -0
  8. package/dist/cli/commands/context.d.ts.map +1 -0
  9. package/dist/cli/commands/context.js +39 -0
  10. package/dist/cli/commands/context.js.map +1 -0
  11. package/dist/cli/commands/daemon.d.ts +4 -0
  12. package/dist/cli/commands/daemon.d.ts.map +1 -0
  13. package/dist/cli/commands/daemon.js +55 -0
  14. package/dist/cli/commands/daemon.js.map +1 -0
  15. package/dist/cli/commands/dashboard.d.ts +2 -0
  16. package/dist/cli/commands/dashboard.d.ts.map +1 -0
  17. package/dist/cli/commands/dashboard.js +24 -0
  18. package/dist/cli/commands/dashboard.js.map +1 -0
  19. package/dist/cli/commands/doctor.d.ts +2 -0
  20. package/dist/cli/commands/doctor.d.ts.map +1 -0
  21. package/dist/cli/commands/doctor.js +130 -0
  22. package/dist/cli/commands/doctor.js.map +1 -0
  23. package/dist/cli/commands/exec.d.ts +2 -0
  24. package/dist/cli/commands/exec.d.ts.map +1 -0
  25. package/dist/cli/commands/exec.js +34 -0
  26. package/dist/cli/commands/exec.js.map +1 -0
  27. package/dist/cli/commands/init.d.ts +2 -0
  28. package/dist/cli/commands/init.d.ts.map +1 -0
  29. package/dist/cli/commands/init.js +71 -0
  30. package/dist/cli/commands/init.js.map +1 -0
  31. package/dist/cli/commands/key.d.ts +2 -0
  32. package/dist/cli/commands/key.d.ts.map +1 -0
  33. package/dist/cli/commands/key.js +39 -0
  34. package/dist/cli/commands/key.js.map +1 -0
  35. package/dist/cli/commands/logs.d.ts +2 -0
  36. package/dist/cli/commands/logs.d.ts.map +1 -0
  37. package/dist/cli/commands/logs.js +26 -0
  38. package/dist/cli/commands/logs.js.map +1 -0
  39. package/dist/cli/commands/mcp.d.ts +4 -0
  40. package/dist/cli/commands/mcp.d.ts.map +1 -0
  41. package/dist/cli/commands/mcp.js +70 -0
  42. package/dist/cli/commands/mcp.js.map +1 -0
  43. package/dist/cli/commands/pair.d.ts +6 -0
  44. package/dist/cli/commands/pair.d.ts.map +1 -0
  45. package/dist/cli/commands/pair.js +208 -0
  46. package/dist/cli/commands/pair.js.map +1 -0
  47. package/dist/cli/commands/peers.d.ts +2 -0
  48. package/dist/cli/commands/peers.d.ts.map +1 -0
  49. package/dist/cli/commands/peers.js +29 -0
  50. package/dist/cli/commands/peers.js.map +1 -0
  51. package/dist/cli/commands/service/linux.d.ts +7 -0
  52. package/dist/cli/commands/service/linux.d.ts.map +1 -0
  53. package/dist/cli/commands/service/linux.js +86 -0
  54. package/dist/cli/commands/service/linux.js.map +1 -0
  55. package/dist/cli/commands/service/macos.d.ts +7 -0
  56. package/dist/cli/commands/service/macos.d.ts.map +1 -0
  57. package/dist/cli/commands/service/macos.js +83 -0
  58. package/dist/cli/commands/service/macos.js.map +1 -0
  59. package/dist/cli/commands/service/windows.d.ts +7 -0
  60. package/dist/cli/commands/service/windows.d.ts.map +1 -0
  61. package/dist/cli/commands/service/windows.js +52 -0
  62. package/dist/cli/commands/service/windows.js.map +1 -0
  63. package/dist/cli/commands/service.d.ts +4 -0
  64. package/dist/cli/commands/service.d.ts.map +1 -0
  65. package/dist/cli/commands/service.js +68 -0
  66. package/dist/cli/commands/service.js.map +1 -0
  67. package/dist/cli/commands/session.d.ts +8 -0
  68. package/dist/cli/commands/session.d.ts.map +1 -0
  69. package/dist/cli/commands/session.js +270 -0
  70. package/dist/cli/commands/session.js.map +1 -0
  71. package/dist/cli/commands/transfer.d.ts +3 -0
  72. package/dist/cli/commands/transfer.d.ts.map +1 -0
  73. package/dist/cli/commands/transfer.js +57 -0
  74. package/dist/cli/commands/transfer.js.map +1 -0
  75. package/dist/cli/index.d.ts +3 -0
  76. package/dist/cli/index.d.ts.map +1 -0
  77. package/dist/cli/index.js +89 -0
  78. package/dist/cli/index.js.map +1 -0
  79. package/dist/cli/package-root.d.ts +27 -0
  80. package/dist/cli/package-root.d.ts.map +1 -0
  81. package/dist/cli/package-root.js +54 -0
  82. package/dist/cli/package-root.js.map +1 -0
  83. package/dist/cli/utils.d.ts +11 -0
  84. package/dist/cli/utils.d.ts.map +1 -0
  85. package/dist/cli/utils.js +48 -0
  86. package/dist/cli/utils.js.map +1 -0
  87. package/dist/daemon/config.d.ts +4 -0
  88. package/dist/daemon/config.d.ts.map +1 -0
  89. package/dist/daemon/config.js +58 -0
  90. package/dist/daemon/config.js.map +1 -0
  91. package/dist/daemon/hooks/permission-hook.mjs +108 -0
  92. package/dist/daemon/index.d.ts +3 -0
  93. package/dist/daemon/index.d.ts.map +1 -0
  94. package/dist/daemon/index.js +3 -0
  95. package/dist/daemon/index.js.map +1 -0
  96. package/dist/daemon/main.d.ts +3 -0
  97. package/dist/daemon/main.d.ts.map +1 -0
  98. package/dist/daemon/main.js +28 -0
  99. package/dist/daemon/main.js.map +1 -0
  100. package/dist/daemon/middleware/auth.d.ts +3 -0
  101. package/dist/daemon/middleware/auth.d.ts.map +1 -0
  102. package/dist/daemon/middleware/auth.js +22 -0
  103. package/dist/daemon/middleware/auth.js.map +1 -0
  104. package/dist/daemon/routes/ai-tasks.d.ts +4 -0
  105. package/dist/daemon/routes/ai-tasks.d.ts.map +1 -0
  106. package/dist/daemon/routes/ai-tasks.js +146 -0
  107. package/dist/daemon/routes/ai-tasks.js.map +1 -0
  108. package/dist/daemon/routes/context.d.ts +4 -0
  109. package/dist/daemon/routes/context.d.ts.map +1 -0
  110. package/dist/daemon/routes/context.js +49 -0
  111. package/dist/daemon/routes/context.js.map +1 -0
  112. package/dist/daemon/routes/execute.d.ts +4 -0
  113. package/dist/daemon/routes/execute.d.ts.map +1 -0
  114. package/dist/daemon/routes/execute.js +74 -0
  115. package/dist/daemon/routes/execute.js.map +1 -0
  116. package/dist/daemon/routes/health.d.ts +15 -0
  117. package/dist/daemon/routes/health.d.ts.map +1 -0
  118. package/dist/daemon/routes/health.js +38 -0
  119. package/dist/daemon/routes/health.js.map +1 -0
  120. package/dist/daemon/routes/pair.d.ts +11 -0
  121. package/dist/daemon/routes/pair.d.ts.map +1 -0
  122. package/dist/daemon/routes/pair.js +149 -0
  123. package/dist/daemon/routes/pair.js.map +1 -0
  124. package/dist/daemon/routes/peers.d.ts +5 -0
  125. package/dist/daemon/routes/peers.d.ts.map +1 -0
  126. package/dist/daemon/routes/peers.js +69 -0
  127. package/dist/daemon/routes/peers.js.map +1 -0
  128. package/dist/daemon/routes/transfer.d.ts +4 -0
  129. package/dist/daemon/routes/transfer.d.ts.map +1 -0
  130. package/dist/daemon/routes/transfer.js +135 -0
  131. package/dist/daemon/routes/transfer.js.map +1 -0
  132. package/dist/daemon/server.d.ts +8 -0
  133. package/dist/daemon/server.d.ts.map +1 -0
  134. package/dist/daemon/server.js +170 -0
  135. package/dist/daemon/server.js.map +1 -0
  136. package/dist/daemon/services/ai-task-manager.d.ts +56 -0
  137. package/dist/daemon/services/ai-task-manager.d.ts.map +1 -0
  138. package/dist/daemon/services/ai-task-manager.js +491 -0
  139. package/dist/daemon/services/ai-task-manager.js.map +1 -0
  140. package/dist/daemon/services/audit-logger.d.ts +16 -0
  141. package/dist/daemon/services/audit-logger.d.ts.map +1 -0
  142. package/dist/daemon/services/audit-logger.js +23 -0
  143. package/dist/daemon/services/audit-logger.js.map +1 -0
  144. package/dist/daemon/services/context-store.d.ts +17 -0
  145. package/dist/daemon/services/context-store.d.ts.map +1 -0
  146. package/dist/daemon/services/context-store.js +97 -0
  147. package/dist/daemon/services/context-store.js.map +1 -0
  148. package/dist/daemon/services/job-manager.d.ts +19 -0
  149. package/dist/daemon/services/job-manager.d.ts.map +1 -0
  150. package/dist/daemon/services/job-manager.js +92 -0
  151. package/dist/daemon/services/job-manager.js.map +1 -0
  152. package/dist/daemon/services/tls-manager.d.ts +33 -0
  153. package/dist/daemon/services/tls-manager.d.ts.map +1 -0
  154. package/dist/daemon/services/tls-manager.js +114 -0
  155. package/dist/daemon/services/tls-manager.js.map +1 -0
  156. package/dist/daemon/utils/which.d.ts +2 -0
  157. package/dist/daemon/utils/which.d.ts.map +1 -0
  158. package/dist/daemon/utils/which.js +18 -0
  159. package/dist/daemon/utils/which.js.map +1 -0
  160. package/dist/dashboard/config.d.ts +8 -0
  161. package/dist/dashboard/config.d.ts.map +1 -0
  162. package/dist/dashboard/config.js +22 -0
  163. package/dist/dashboard/config.js.map +1 -0
  164. package/dist/dashboard/public/app.js +120 -0
  165. package/dist/dashboard/public/icon-192.png +0 -0
  166. package/dist/dashboard/public/icon-512.png +0 -0
  167. package/dist/dashboard/public/index.html +85 -0
  168. package/dist/dashboard/public/manifest.json +12 -0
  169. package/dist/dashboard/public/style.css +784 -0
  170. package/dist/dashboard/public/sw.js +31 -0
  171. package/dist/dashboard/public/views/ai-tasks.js +679 -0
  172. package/dist/dashboard/public/views/context.js +167 -0
  173. package/dist/dashboard/public/views/messages.js +263 -0
  174. package/dist/dashboard/public/views/overview.js +228 -0
  175. package/dist/dashboard/public/views/peers.js +136 -0
  176. package/dist/dashboard/public/views/terminal.js +153 -0
  177. package/dist/dashboard/routes/ai-tasks.d.ts +3 -0
  178. package/dist/dashboard/routes/ai-tasks.d.ts.map +1 -0
  179. package/dist/dashboard/routes/ai-tasks.js +193 -0
  180. package/dist/dashboard/routes/ai-tasks.js.map +1 -0
  181. package/dist/dashboard/routes/messages.d.ts +3 -0
  182. package/dist/dashboard/routes/messages.d.ts.map +1 -0
  183. package/dist/dashboard/routes/messages.js +137 -0
  184. package/dist/dashboard/routes/messages.js.map +1 -0
  185. package/dist/dashboard/routes/peer-utils.d.ts +17 -0
  186. package/dist/dashboard/routes/peer-utils.d.ts.map +1 -0
  187. package/dist/dashboard/routes/peer-utils.js +193 -0
  188. package/dist/dashboard/routes/peer-utils.js.map +1 -0
  189. package/dist/dashboard/routes/peers-all.d.ts +3 -0
  190. package/dist/dashboard/routes/peers-all.d.ts.map +1 -0
  191. package/dist/dashboard/routes/peers-all.js +8 -0
  192. package/dist/dashboard/routes/peers-all.js.map +1 -0
  193. package/dist/dashboard/routes/proxy.d.ts +3 -0
  194. package/dist/dashboard/routes/proxy.d.ts.map +1 -0
  195. package/dist/dashboard/routes/proxy.js +59 -0
  196. package/dist/dashboard/routes/proxy.js.map +1 -0
  197. package/dist/dashboard/routes/sessions.d.ts +3 -0
  198. package/dist/dashboard/routes/sessions.d.ts.map +1 -0
  199. package/dist/dashboard/routes/sessions.js +64 -0
  200. package/dist/dashboard/routes/sessions.js.map +1 -0
  201. package/dist/dashboard/routes/sse.d.ts +3 -0
  202. package/dist/dashboard/routes/sse.d.ts.map +1 -0
  203. package/dist/dashboard/routes/sse.js +49 -0
  204. package/dist/dashboard/routes/sse.js.map +1 -0
  205. package/dist/dashboard/routes/status.d.ts +3 -0
  206. package/dist/dashboard/routes/status.d.ts.map +1 -0
  207. package/dist/dashboard/routes/status.js +38 -0
  208. package/dist/dashboard/routes/status.js.map +1 -0
  209. package/dist/dashboard/server.d.ts +3 -0
  210. package/dist/dashboard/server.d.ts.map +1 -0
  211. package/dist/dashboard/server.js +77 -0
  212. package/dist/dashboard/server.js.map +1 -0
  213. package/dist/dashboard/session-manager.d.ts +17 -0
  214. package/dist/dashboard/session-manager.d.ts.map +1 -0
  215. package/dist/dashboard/session-manager.js +225 -0
  216. package/dist/dashboard/session-manager.js.map +1 -0
  217. package/dist/discovery/health-checker.d.ts +15 -0
  218. package/dist/discovery/health-checker.d.ts.map +1 -0
  219. package/dist/discovery/health-checker.js +47 -0
  220. package/dist/discovery/health-checker.js.map +1 -0
  221. package/dist/discovery/index.d.ts +4 -0
  222. package/dist/discovery/index.d.ts.map +1 -0
  223. package/dist/discovery/index.js +4 -0
  224. package/dist/discovery/index.js.map +1 -0
  225. package/dist/discovery/mdns.d.ts +21 -0
  226. package/dist/discovery/mdns.d.ts.map +1 -0
  227. package/dist/discovery/mdns.js +83 -0
  228. package/dist/discovery/mdns.js.map +1 -0
  229. package/dist/discovery/peer-registry.d.ts +18 -0
  230. package/dist/discovery/peer-registry.d.ts.map +1 -0
  231. package/dist/discovery/peer-registry.js +81 -0
  232. package/dist/discovery/peer-registry.js.map +1 -0
  233. package/dist/mcp-server/daemon-client.d.ts +69 -0
  234. package/dist/mcp-server/daemon-client.d.ts.map +1 -0
  235. package/dist/mcp-server/daemon-client.js +281 -0
  236. package/dist/mcp-server/daemon-client.js.map +1 -0
  237. package/dist/mcp-server/index.d.ts +3 -0
  238. package/dist/mcp-server/index.d.ts.map +1 -0
  239. package/dist/mcp-server/index.js +406 -0
  240. package/dist/mcp-server/index.js.map +1 -0
  241. package/dist/protocol/constants.d.ts +66 -0
  242. package/dist/protocol/constants.d.ts.map +1 -0
  243. package/dist/protocol/constants.js +66 -0
  244. package/dist/protocol/constants.js.map +1 -0
  245. package/dist/protocol/errors.d.ts +47 -0
  246. package/dist/protocol/errors.d.ts.map +1 -0
  247. package/dist/protocol/errors.js +62 -0
  248. package/dist/protocol/errors.js.map +1 -0
  249. package/dist/protocol/index.d.ts +5 -0
  250. package/dist/protocol/index.d.ts.map +1 -0
  251. package/dist/protocol/index.js +5 -0
  252. package/dist/protocol/index.js.map +1 -0
  253. package/dist/protocol/schemas.d.ts +209 -0
  254. package/dist/protocol/schemas.d.ts.map +1 -0
  255. package/dist/protocol/schemas.js +115 -0
  256. package/dist/protocol/schemas.js.map +1 -0
  257. package/dist/protocol/types.d.ts +302 -0
  258. package/dist/protocol/types.d.ts.map +1 -0
  259. package/dist/protocol/types.js +2 -0
  260. package/dist/protocol/types.js.map +1 -0
  261. package/package.json +50 -0
  262. package/scripts/postinstall.mjs +42 -0
@@ -0,0 +1,679 @@
1
+ import { registerView, dashboardApi, escapeHtml, formatTime } from '/app.js';
2
+
3
+ let refreshTimer = null;
4
+ let selectedTask = null; // { taskId, port, address }
5
+ let eventSource = null;
6
+ let taskFinished = false; // true once we receive an exit event
7
+ let resolvedRequestIds = new Set(); // track approved/denied requests to skip stale replays
8
+ let currentPendingRequestId = null; // the currently active pending request from the daemon
9
+
10
+ function mount(container) {
11
+ container.innerHTML = `
12
+ <div class="section-header">AI Tasks</div>
13
+
14
+ <div class="tabs">
15
+ <div class="tab active" data-tab="dispatch">Dispatch</div>
16
+ <div class="tab" data-tab="tasks">Active Tasks</div>
17
+ </div>
18
+
19
+ <div id="ai-content"></div>
20
+ `;
21
+
22
+ document.querySelectorAll('.tab').forEach(t => {
23
+ t.addEventListener('click', () => {
24
+ document.querySelectorAll('.tab').forEach(x => x.classList.toggle('active', x === t));
25
+ if (t.dataset.tab === 'dispatch') {
26
+ closeStream();
27
+ selectedTask = null;
28
+ renderDispatch();
29
+ } else {
30
+ renderTasks();
31
+ }
32
+ });
33
+ });
34
+
35
+ renderDispatch();
36
+ refreshTimer = setInterval(refreshTasksIfVisible, 5000);
37
+ }
38
+
39
+ function unmount() {
40
+ closeStream();
41
+ if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
42
+ }
43
+
44
+ // ── Dispatch Form ──
45
+
46
+ async function renderDispatch() {
47
+ const content = document.getElementById('ai-content');
48
+ content.innerHTML = `
49
+ <div class="form-row">
50
+ <div class="form-group" style="flex:1">
51
+ <div class="form-label">Target Peer</div>
52
+ <select class="input" id="ai-target">
53
+ <option value="">Loading peers...</option>
54
+ </select>
55
+ </div>
56
+ <div class="form-group">
57
+ <div class="form-label">Permission Mode</div>
58
+ <select class="input" id="ai-perm-mode">
59
+ <option value="default">Default (Human Approves)</option>
60
+ <option value="acceptEdits">Accept Edits</option>
61
+ <option value="bypassPermissions">Bypass All</option>
62
+ <option value="dontAsk">Don't Ask (Skip)</option>
63
+ </select>
64
+ </div>
65
+ </div>
66
+ <div class="form-row">
67
+ <div class="form-group">
68
+ <div class="form-label">Model</div>
69
+ <select class="input" id="ai-model">
70
+ <option value="">Default</option>
71
+ <option value="sonnet">Sonnet</option>
72
+ <option value="opus">Opus</option>
73
+ <option value="haiku">Haiku</option>
74
+ </select>
75
+ </div>
76
+ <div class="form-group" style="flex:1">
77
+ <div class="form-label">Working Dir (optional)</div>
78
+ <input class="input" id="ai-cwd" placeholder="e.g. /home/user/project">
79
+ </div>
80
+ </div>
81
+ <div class="form-row">
82
+ <div class="form-group" style="flex:1">
83
+ <div class="form-label">Prompt</div>
84
+ <textarea class="textarea input" id="ai-prompt" rows="5" placeholder="Describe the task for Claude..."></textarea>
85
+ </div>
86
+ </div>
87
+ <button class="btn btn-primary" id="btn-dispatch">Dispatch Task</button>
88
+ <div id="dispatch-status" class="mt-1 text-sm"></div>
89
+ `;
90
+
91
+ loadPeers();
92
+ document.getElementById('btn-dispatch').addEventListener('click', dispatchTask);
93
+ }
94
+
95
+ async function loadPeers() {
96
+ const sel = document.getElementById('ai-target');
97
+ if (!sel) return;
98
+ try {
99
+ const data = await dashboardApi('/peers/all');
100
+ const peers = (data.peers || []).filter(p => p.status === 'online');
101
+ // Also add local sessions
102
+ const sessData = await dashboardApi('/sessions');
103
+ const localSessions = [];
104
+ if (sessData.main && sessData.main.status === 'running') localSessions.push(sessData.main);
105
+ localSessions.push(...(sessData.sessions || []).filter(s => s.status === 'running'));
106
+
107
+ let options = '';
108
+ // Local sessions first
109
+ options += '<optgroup label="Local Sessions">';
110
+ for (const s of localSessions) {
111
+ options += `<option value="${s.port}|127.0.0.1">${escapeHtml(s.hostname)} :${s.port}</option>`;
112
+ }
113
+ options += '</optgroup>';
114
+
115
+ // Remote peers (exclude local ones)
116
+ const remotePeers = peers.filter(p => p.address !== '127.0.0.1' && p.address !== 'localhost');
117
+ if (remotePeers.length > 0) {
118
+ options += '<optgroup label="Remote Peers">';
119
+ for (const p of remotePeers) {
120
+ options += `<option value="${p.port}|${escapeHtml(p.address)}">${escapeHtml(p.hostname)} (${escapeHtml(p.address)}:${p.port})</option>`;
121
+ }
122
+ options += '</optgroup>';
123
+ }
124
+
125
+ sel.innerHTML = options || '<option value="">No peers online</option>';
126
+ } catch {
127
+ sel.innerHTML = '<option value="">Failed to load</option>';
128
+ }
129
+ }
130
+
131
+ async function dispatchTask() {
132
+ const targetVal = document.getElementById('ai-target').value;
133
+ const prompt = document.getElementById('ai-prompt').value.trim();
134
+ const permissionMode = document.getElementById('ai-perm-mode').value;
135
+ const model = document.getElementById('ai-model').value;
136
+ const cwd = document.getElementById('ai-cwd').value.trim();
137
+ const statusEl = document.getElementById('dispatch-status');
138
+
139
+ if (!targetVal || !prompt) {
140
+ statusEl.innerHTML = '<span class="text-red">Select a target and enter a prompt</span>';
141
+ return;
142
+ }
143
+
144
+ const [port, address] = targetVal.split('|');
145
+
146
+ statusEl.innerHTML = '<span class="text-muted">Dispatching...</span>';
147
+
148
+ try {
149
+ const body = {
150
+ targetPort: parseInt(port),
151
+ targetAddress: address,
152
+ prompt,
153
+ permissionMode,
154
+ };
155
+ if (model) body.model = model;
156
+ if (cwd) body.cwd = cwd;
157
+
158
+ const result = await dashboardApi('/ai-tasks/dispatch', {
159
+ method: 'POST',
160
+ body: JSON.stringify(body),
161
+ });
162
+
163
+ if (result.taskId) {
164
+ statusEl.innerHTML = `<span class="text-green">Dispatched! Task: ${escapeHtml(result.taskId.slice(0, 8))}...</span>`;
165
+ // Switch to tasks tab and open the stream
166
+ selectedTask = { taskId: result.taskId, port: parseInt(port), address };
167
+ document.querySelectorAll('.tab').forEach(x => x.classList.toggle('active', x.dataset.tab === 'tasks'));
168
+ renderTaskStream();
169
+ } else {
170
+ statusEl.innerHTML = `<span class="text-red">${escapeHtml(result.error || 'Unknown error')}</span>`;
171
+ }
172
+ } catch (err) {
173
+ statusEl.innerHTML = `<span class="text-red">Failed: ${escapeHtml(err.message)}</span>`;
174
+ }
175
+ }
176
+
177
+ // ── Active Tasks List ──
178
+
179
+ async function renderTasks() {
180
+ if (selectedTask) {
181
+ renderTaskStream();
182
+ return;
183
+ }
184
+
185
+ const content = document.getElementById('ai-content');
186
+ content.innerHTML = '<div class="text-muted text-sm">Loading tasks...</div>';
187
+
188
+ try {
189
+ const data = await dashboardApi('/ai-tasks/all');
190
+ const tasks = data.tasks || [];
191
+
192
+ if (tasks.length === 0) {
193
+ content.innerHTML = '<div class="empty">No AI tasks dispatched yet</div>';
194
+ return;
195
+ }
196
+
197
+ content.innerHTML = tasks.map(t => {
198
+ const badgeClass = statusBadgeClass(t.status);
199
+ const promptPreview = t.prompt.length > 100 ? t.prompt.slice(0, 100) + '...' : t.prompt;
200
+ const needsAttention = t.status === 'waiting_approval';
201
+
202
+ return `
203
+ <div class="msg-row${needsAttention ? ' needs-attention' : ''}" onclick="window.__selectTask('${escapeHtml(t.taskId)}', ${t._sourcePort || 19532}, '${escapeHtml(t._sourceAddress || '127.0.0.1')}')">
204
+ <div class="msg-header">
205
+ <span class="badge ${badgeClass}${needsAttention ? ' pulse' : ''}">${escapeHtml(t.status)}</span>
206
+ <span class="msg-from">${escapeHtml(t._sourceHostname || 'unknown')}</span>
207
+ <span class="msg-time">${formatTime(t.startedAt)}</span>
208
+ </div>
209
+ <div class="msg-body">${escapeHtml(promptPreview)}</div>
210
+ ${t.model ? `<div class="text-xs text-muted mt-1">Model: ${escapeHtml(t.model)}</div>` : ''}
211
+ </div>
212
+ `;
213
+ }).join('');
214
+ } catch (err) {
215
+ content.innerHTML = `<div class="empty">${escapeHtml(err.message)}</div>`;
216
+ }
217
+ }
218
+
219
+ function refreshTasksIfVisible() {
220
+ const activeTab = document.querySelector('.tab.active');
221
+ if (activeTab?.dataset.tab === 'tasks' && !selectedTask) {
222
+ renderTasks();
223
+ }
224
+ }
225
+
226
+ // ── Task Stream + Approval ──
227
+
228
+ async function renderTaskStream() {
229
+ if (!selectedTask) return;
230
+ const content = document.getElementById('ai-content');
231
+ const { taskId, port } = selectedTask;
232
+
233
+ content.innerHTML = `
234
+ <div class="flex justify-between items-center mb-1">
235
+ <button class="btn btn-sm" id="btn-back-tasks" style="border-color:var(--text-muted)"><svg width="14" height="14" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:middle;margin-right:4px"><polyline points="12,3 6,9 12,15"/></svg>Back</button>
236
+ <button class="btn btn-sm btn-danger" id="btn-cancel-task">Cancel</button>
237
+ </div>
238
+ <div class="task-info">
239
+ <div class="task-info-header">
240
+ <span class="badge badge-cyan" id="task-status-badge">loading</span>
241
+ <span class="text-xs font-mono text-muted">Task: ${escapeHtml(taskId.slice(0, 12))}...</span>
242
+ </div>
243
+ <div class="task-info-prompt" id="task-prompt">Loading...</div>
244
+ <div class="task-info-meta" id="task-meta"></div>
245
+ </div>
246
+ <div class="ai-terminal" id="ai-output"></div>
247
+ <div id="ai-approval" style="display:none"></div>
248
+ <div id="ai-followup" style="display:none"></div>
249
+ `;
250
+
251
+ document.getElementById('btn-back-tasks').addEventListener('click', () => {
252
+ closeStream();
253
+ selectedTask = null;
254
+ renderTasks();
255
+ });
256
+ document.getElementById('btn-cancel-task').addEventListener('click', cancelTask);
257
+
258
+ // Fetch task info for header
259
+ try {
260
+ const data = await dashboardApi('/ai-tasks/all');
261
+ const task = (data.tasks || []).find(t => t.taskId === taskId);
262
+ if (task) {
263
+ updateStatusBadge(task.status);
264
+ document.getElementById('task-prompt').textContent = task.prompt;
265
+ const meta = [];
266
+ if (task.model) meta.push(`Model: ${task.model}`);
267
+ if (task._sourceHostname) meta.push(`Host: ${task._sourceHostname}`);
268
+ if (task.startedAt) meta.push(formatTime(task.startedAt));
269
+ const metaEl = document.getElementById('task-meta');
270
+ if (metaEl) metaEl.textContent = meta.join(' \u00b7 ');
271
+ }
272
+ } catch {}
273
+
274
+ connectStream(taskId, port, selectedTask.address);
275
+ }
276
+
277
+ function updateStatusBadge(status) {
278
+ const badge = document.getElementById('task-status-badge');
279
+ if (!badge) return;
280
+ badge.textContent = status;
281
+ badge.className = `badge ${statusBadgeClass(status)}`;
282
+ }
283
+
284
+ async function connectStream(taskId, port, address) {
285
+ closeStream();
286
+ taskFinished = false;
287
+ resolvedRequestIds = new Set();
288
+ currentPendingRequestId = null;
289
+
290
+ // Check task state before connecting — detect finished tasks and
291
+ // identify which permission request (if any) is actually pending
292
+ try {
293
+ const data = await dashboardApi('/ai-tasks/all');
294
+ const task = (data.tasks || []).find(t => t.taskId === taskId);
295
+ if (task) {
296
+ if (task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled') {
297
+ taskFinished = true;
298
+ }
299
+ // Track which request is actually pending right now
300
+ if (task.pendingApproval?.requestId) {
301
+ currentPendingRequestId = task.pendingApproval.requestId;
302
+ }
303
+ }
304
+ } catch {}
305
+
306
+ const addrParam = address ? `&address=${encodeURIComponent(address)}` : '';
307
+ const url = `/dashboard/api/ai-tasks/stream/${port}/${encodeURIComponent(taskId)}?since=0${addrParam}`;
308
+ eventSource = new EventSource(url);
309
+
310
+ eventSource.onmessage = (e) => {
311
+ try {
312
+ const event = JSON.parse(e.data);
313
+ handleStreamEvent(event);
314
+ } catch {}
315
+ };
316
+
317
+ eventSource.onerror = () => {
318
+ closeStream();
319
+ // Auto-reconnect if the task is still running
320
+ if (!taskFinished && selectedTask) {
321
+ appendLine('system', '[Reconnecting...]');
322
+ setTimeout(() => {
323
+ if (!taskFinished && selectedTask && selectedTask.taskId === taskId) {
324
+ connectStream(taskId, port, address);
325
+ }
326
+ }, 2000);
327
+ } else {
328
+ appendLine('system', '[Connection closed]');
329
+ }
330
+ };
331
+ }
332
+
333
+ function handleStreamEvent(event) {
334
+ const { type, data } = event;
335
+
336
+ // Filter system init and user echo noise
337
+ if (type === 'text' || type === 'default') {
338
+ if (typeof data === 'object' && data !== null) {
339
+ if (data.type === 'system' && data.subtype === 'init') return;
340
+ if (data.type === 'user') return;
341
+ }
342
+ if (typeof data === 'string') {
343
+ // Try parsing JSON strings that are init/user payloads
344
+ try {
345
+ const parsed = JSON.parse(data);
346
+ if (parsed.type === 'system' && parsed.subtype === 'init') return;
347
+ if (parsed.type === 'user') return;
348
+ } catch {}
349
+ // Skip empty text
350
+ if (!data.trim()) return;
351
+ }
352
+ }
353
+
354
+ switch (type) {
355
+ case 'text':
356
+ appendLine('stdout', typeof data === 'string' ? data : (data?.text || JSON.stringify(data)));
357
+ break;
358
+ case 'thinking':
359
+ appendLine('thinking', typeof data === 'string' ? data : JSON.stringify(data));
360
+ break;
361
+ case 'tool_use': {
362
+ const name = data?.name || 'tool';
363
+ const input = data?.input;
364
+ let desc = '';
365
+ if (input) {
366
+ // Show concise tool description
367
+ if (input.command) desc = input.command;
368
+ else if (input.file_path) desc = input.file_path;
369
+ else if (input.pattern) desc = input.pattern;
370
+ else if (input.query) desc = input.query;
371
+ else if (input.url) desc = input.url;
372
+ else {
373
+ const str = typeof input === 'string' ? input : JSON.stringify(input);
374
+ desc = str.length > 100 ? str.slice(0, 100) + '...' : str;
375
+ }
376
+ }
377
+ appendLine('tool-use', `\u25b6 ${name}${desc ? ': ' + desc : ''}`);
378
+ break;
379
+ }
380
+ case 'tool_result': {
381
+ const result = formatToolResult(data);
382
+ if (result) appendLine('tool-result', result);
383
+ break;
384
+ }
385
+ case 'permission_request': {
386
+ const reqId = data?.requestId;
387
+ if (!taskFinished && reqId && !resolvedRequestIds.has(reqId)) {
388
+ // Only show if this is the actually pending request (or a new live one)
389
+ if (!currentPendingRequestId || reqId === currentPendingRequestId) {
390
+ showApprovalBanner(data);
391
+ }
392
+ }
393
+ break;
394
+ }
395
+ case 'status':
396
+ updateStatusBadge(data?.status || 'running');
397
+ // When a permission is resolved, hide the banner and track it
398
+ if (data?.approved !== undefined) {
399
+ const approvalEl = document.getElementById('ai-approval');
400
+ if (approvalEl) { approvalEl.style.display = 'none'; approvalEl.innerHTML = ''; }
401
+ // After seeing an approval status, any prior permission_request is resolved
402
+ currentPendingRequestId = null;
403
+ }
404
+ break;
405
+ case 'error':
406
+ appendLine('stderr', typeof data === 'string' ? data : JSON.stringify(data));
407
+ break;
408
+ case 'result': {
409
+ const parts = [];
410
+ if (data?.cost) {
411
+ parts.push(`Cost: $${data.cost.input || '?'} in + $${data.cost.output || '?'} out`);
412
+ }
413
+ if (data?.duration) parts.push(`Duration: ${formatDuration(data.duration)}`);
414
+ if (parts.length > 0) appendLine('result', parts.join(' \u00b7 '));
415
+ updateStatusBadge('completed');
416
+ showFollowUpInput();
417
+ break;
418
+ }
419
+ case 'exit':
420
+ taskFinished = true;
421
+ updateStatusBadge(data?.exitCode === 0 ? 'completed' : 'failed');
422
+ if (data?.exitCode !== 0) {
423
+ appendLine('stderr', `Exited with code ${data?.exitCode ?? '?'}`);
424
+ }
425
+ // Hide any stale approval banner
426
+ const approvalEl = document.getElementById('ai-approval');
427
+ if (approvalEl) { approvalEl.style.display = 'none'; approvalEl.innerHTML = ''; }
428
+ // Show follow-up input for completed tasks
429
+ if (data?.exitCode === 0) showFollowUpInput();
430
+ closeStream();
431
+ break;
432
+ default:
433
+ // Skip unknown event types silently instead of dumping raw JSON
434
+ break;
435
+ }
436
+ }
437
+
438
+ function showApprovalBanner(approval) {
439
+ const el = document.getElementById('ai-approval');
440
+ if (!el) return;
441
+
442
+ el.style.display = 'block';
443
+ el.innerHTML = `
444
+ <div class="approval-banner">
445
+ <div class="approval-header">
446
+ <span class="badge badge-amber pulse">APPROVAL NEEDED</span>
447
+ <span class="text-sm">${escapeHtml(approval.toolName || 'Unknown tool')}</span>
448
+ </div>
449
+ <div class="approval-desc">${escapeHtml(approval.description || 'Claude wants to use a tool')}</div>
450
+ <div class="approval-input">
451
+ <pre>${escapeHtml(typeof approval.toolInput === 'string' ? approval.toolInput : JSON.stringify(approval.toolInput, null, 2))}</pre>
452
+ </div>
453
+ <div class="approval-actions">
454
+ <button class="btn btn-success" id="btn-approve">Approve</button>
455
+ <button class="btn btn-danger" id="btn-deny">Deny</button>
456
+ </div>
457
+ </div>
458
+ `;
459
+
460
+ document.getElementById('btn-approve').addEventListener('click', () => {
461
+ sendApproval(approval.requestId, true);
462
+ });
463
+ document.getElementById('btn-deny').addEventListener('click', () => {
464
+ sendApproval(approval.requestId, false);
465
+ });
466
+
467
+ // Scroll to make it visible
468
+ el.scrollIntoView({ behavior: 'smooth' });
469
+ }
470
+
471
+ async function sendApproval(requestId, approved) {
472
+ if (!selectedTask) return;
473
+ const { taskId, port, address } = selectedTask;
474
+ const addrParam = address ? `?address=${encodeURIComponent(address)}` : '';
475
+
476
+ const el = document.getElementById('ai-approval');
477
+ if (el) {
478
+ el.innerHTML = `<div class="text-muted text-sm">${approved ? 'Approving...' : 'Denying...'}</div>`;
479
+ }
480
+
481
+ try {
482
+ await dashboardApi(`/ai-tasks/approve/${port}/${encodeURIComponent(taskId)}${addrParam}`, {
483
+ method: 'POST',
484
+ body: JSON.stringify({ requestId, approved }),
485
+ });
486
+ resolvedRequestIds.add(requestId);
487
+ currentPendingRequestId = null;
488
+ if (el) {
489
+ el.style.display = 'none';
490
+ el.innerHTML = '';
491
+ }
492
+ appendLine('system', approved ? 'Approved — resuming...' : 'Denied');
493
+ } catch (err) {
494
+ appendLine('stderr', `Approval failed: ${err.message}`);
495
+ }
496
+ }
497
+
498
+ async function cancelTask() {
499
+ if (!selectedTask) return;
500
+ const { taskId, port, address } = selectedTask;
501
+ const addrParam = address ? `?address=${encodeURIComponent(address)}` : '';
502
+ try {
503
+ await dashboardApi(`/ai-tasks/cancel/${port}/${encodeURIComponent(taskId)}${addrParam}`, { method: 'DELETE' });
504
+ appendLine('system', 'Task cancelled');
505
+ } catch (err) {
506
+ appendLine('stderr', `Cancel failed: ${err.message}`);
507
+ }
508
+ }
509
+
510
+ function closeStream() {
511
+ if (eventSource) {
512
+ eventSource.close();
513
+ eventSource = null;
514
+ }
515
+ }
516
+
517
+ // ── Follow-up Input ──
518
+
519
+ function showFollowUpInput() {
520
+ if (!selectedTask) return;
521
+ const el = document.getElementById('ai-followup');
522
+ if (!el) return;
523
+ el.style.display = 'block';
524
+ el.innerHTML = `
525
+ <div class="followup-bar">
526
+ <textarea class="textarea input" id="followup-prompt" rows="2" placeholder="Send a follow-up message..."></textarea>
527
+ <div class="followup-actions"><button class="btn btn-primary btn-sm" id="btn-followup">Send</button></div>
528
+ </div>
529
+ `;
530
+ document.getElementById('btn-followup').addEventListener('click', sendFollowUp);
531
+ document.getElementById('followup-prompt').addEventListener('keydown', (e) => {
532
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendFollowUp(); }
533
+ });
534
+ el.scrollIntoView({ behavior: 'smooth' });
535
+ }
536
+
537
+ function collectConversationHistory() {
538
+ const output = document.getElementById('ai-output');
539
+ if (!output) return '';
540
+ const lines = [];
541
+ for (const el of output.children) {
542
+ if (el.classList.contains('ai-stdout')) {
543
+ lines.push(`Assistant: ${el.textContent}`);
544
+ } else if (el.classList.contains('ai-tool-use')) {
545
+ lines.push(`[Tool: ${el.textContent}]`);
546
+ }
547
+ }
548
+ return lines.join('\n');
549
+ }
550
+
551
+ async function sendFollowUp() {
552
+ if (!selectedTask) return;
553
+ const promptEl = document.getElementById('followup-prompt');
554
+ const prompt = promptEl?.value?.trim();
555
+ if (!prompt) return;
556
+
557
+ const { port, address } = selectedTask;
558
+ const btnEl = document.getElementById('btn-followup');
559
+ if (btnEl) btnEl.disabled = true;
560
+
561
+ // Build context-enriched prompt with conversation history
562
+ const history = collectConversationHistory();
563
+ const contextPrompt = history
564
+ ? `Here is our conversation so far:\n\n${history}\n\nUser follow-up: ${prompt}`
565
+ : prompt;
566
+
567
+ try {
568
+ const body = {
569
+ targetPort: port,
570
+ targetAddress: address,
571
+ prompt: contextPrompt,
572
+ permissionMode: 'default',
573
+ };
574
+
575
+ const result = await dashboardApi('/ai-tasks/dispatch', {
576
+ method: 'POST',
577
+ body: JSON.stringify(body),
578
+ });
579
+
580
+ if (result.taskId) {
581
+ // Hide follow-up bar, switch to new task stream
582
+ const el = document.getElementById('ai-followup');
583
+ if (el) { el.style.display = 'none'; el.innerHTML = ''; }
584
+ selectedTask = { taskId: result.taskId, port, address };
585
+ appendLine('system', `Follow-up dispatched: ${result.taskId.slice(0, 8)}...`);
586
+ taskFinished = false;
587
+ connectStream(result.taskId, port, address);
588
+ }
589
+ } catch (err) {
590
+ appendLine('stderr', `Follow-up failed: ${err.message}`);
591
+ if (btnEl) btnEl.disabled = false;
592
+ }
593
+ }
594
+
595
+ function formatDuration(ms) {
596
+ if (!ms || ms < 0) return '—';
597
+ const s = Math.floor(ms / 1000);
598
+ const m = Math.floor(s / 60);
599
+ const h = Math.floor(m / 60);
600
+ if (h > 0) return `${h}h ${m % 60}m ${s % 60}s`;
601
+ if (m > 0) return `${m}m ${s % 60}s`;
602
+ return `${s}s`;
603
+ }
604
+
605
+ // ── Terminal Helpers ──
606
+
607
+ function renderMarkdown(text) {
608
+ // Escape HTML first, then apply markdown transformations
609
+ let html = escapeHtml(text);
610
+ // Code blocks (``` ... ```)
611
+ html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
612
+ // Inline code
613
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
614
+ // Bold (**text** or __text__)
615
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
616
+ html = html.replace(/__(.+?)__/g, '<strong>$1</strong>');
617
+ // Italic (*text* or _text_) — but not inside words with underscores
618
+ html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
619
+ // Headers (# text)
620
+ html = html.replace(/^### (.+)$/gm, '<strong style="font-size:1em">$1</strong>');
621
+ html = html.replace(/^## (.+)$/gm, '<strong style="font-size:1.05em">$1</strong>');
622
+ html = html.replace(/^# (.+)$/gm, '<strong style="font-size:1.1em">$1</strong>');
623
+ // Bullet lists
624
+ html = html.replace(/^[-*] (.+)$/gm, '&bull; $1');
625
+ // Numbered lists
626
+ html = html.replace(/^\d+\. (.+)$/gm, '&middot; $1');
627
+ // Line breaks
628
+ html = html.replace(/\n/g, '<br>');
629
+ return html;
630
+ }
631
+
632
+ function appendLine(cls, text, isResult = false) {
633
+ const output = document.getElementById('ai-output');
634
+ if (!output) return;
635
+ const line = document.createElement('div');
636
+ line.className = `ai-line ai-${cls}`;
637
+ if (isResult || cls === 'stdout') {
638
+ line.innerHTML = renderMarkdown(text);
639
+ line.style.whiteSpace = 'pre-wrap';
640
+ } else {
641
+ line.textContent = text;
642
+ }
643
+ output.appendChild(line);
644
+ output.scrollTop = output.scrollHeight;
645
+ }
646
+
647
+ function truncateJson(obj) {
648
+ if (!obj) return '{}';
649
+ const str = typeof obj === 'string' ? obj : JSON.stringify(obj);
650
+ return str.length > 200 ? str.slice(0, 200) + '...' : str;
651
+ }
652
+
653
+ function formatToolResult(data) {
654
+ if (!data) return '';
655
+ const content = data.content || data.output || '';
656
+ if (!content) return '';
657
+ const str = typeof content === 'string' ? content : JSON.stringify(content);
658
+ if (!str.trim()) return '';
659
+ return str.length > 200 ? str.slice(0, 200) + '...' : str;
660
+ }
661
+
662
+ function statusBadgeClass(status) {
663
+ switch (status) {
664
+ case 'running': return 'badge-cyan';
665
+ case 'waiting_approval': return 'badge-amber';
666
+ case 'completed': return 'badge-green';
667
+ case 'failed': case 'cancelled': return 'badge-red';
668
+ default: return 'badge-cyan';
669
+ }
670
+ }
671
+
672
+ // ── Global handlers ──
673
+
674
+ window.__selectTask = (taskId, port, address) => {
675
+ selectedTask = { taskId, port, address: address || '127.0.0.1' };
676
+ renderTaskStream();
677
+ };
678
+
679
+ registerView('ai-tasks', { mount, unmount });