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.
- package/LICENSE +190 -0
- package/README.md +425 -0
- package/dist/cli/commands/connect.d.ts +2 -0
- package/dist/cli/commands/connect.d.ts.map +1 -0
- package/dist/cli/commands/connect.js +120 -0
- package/dist/cli/commands/connect.js.map +1 -0
- package/dist/cli/commands/context.d.ts +2 -0
- package/dist/cli/commands/context.d.ts.map +1 -0
- package/dist/cli/commands/context.js +39 -0
- package/dist/cli/commands/context.js.map +1 -0
- package/dist/cli/commands/daemon.d.ts +4 -0
- package/dist/cli/commands/daemon.d.ts.map +1 -0
- package/dist/cli/commands/daemon.js +55 -0
- package/dist/cli/commands/daemon.js.map +1 -0
- package/dist/cli/commands/dashboard.d.ts +2 -0
- package/dist/cli/commands/dashboard.d.ts.map +1 -0
- package/dist/cli/commands/dashboard.js +24 -0
- package/dist/cli/commands/dashboard.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +130 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/exec.d.ts +2 -0
- package/dist/cli/commands/exec.d.ts.map +1 -0
- package/dist/cli/commands/exec.js +34 -0
- package/dist/cli/commands/exec.js.map +1 -0
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +71 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/key.d.ts +2 -0
- package/dist/cli/commands/key.d.ts.map +1 -0
- package/dist/cli/commands/key.js +39 -0
- package/dist/cli/commands/key.js.map +1 -0
- package/dist/cli/commands/logs.d.ts +2 -0
- package/dist/cli/commands/logs.d.ts.map +1 -0
- package/dist/cli/commands/logs.js +26 -0
- package/dist/cli/commands/logs.js.map +1 -0
- package/dist/cli/commands/mcp.d.ts +4 -0
- package/dist/cli/commands/mcp.d.ts.map +1 -0
- package/dist/cli/commands/mcp.js +70 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/pair.d.ts +6 -0
- package/dist/cli/commands/pair.d.ts.map +1 -0
- package/dist/cli/commands/pair.js +208 -0
- package/dist/cli/commands/pair.js.map +1 -0
- package/dist/cli/commands/peers.d.ts +2 -0
- package/dist/cli/commands/peers.d.ts.map +1 -0
- package/dist/cli/commands/peers.js +29 -0
- package/dist/cli/commands/peers.js.map +1 -0
- package/dist/cli/commands/service/linux.d.ts +7 -0
- package/dist/cli/commands/service/linux.d.ts.map +1 -0
- package/dist/cli/commands/service/linux.js +86 -0
- package/dist/cli/commands/service/linux.js.map +1 -0
- package/dist/cli/commands/service/macos.d.ts +7 -0
- package/dist/cli/commands/service/macos.d.ts.map +1 -0
- package/dist/cli/commands/service/macos.js +83 -0
- package/dist/cli/commands/service/macos.js.map +1 -0
- package/dist/cli/commands/service/windows.d.ts +7 -0
- package/dist/cli/commands/service/windows.d.ts.map +1 -0
- package/dist/cli/commands/service/windows.js +52 -0
- package/dist/cli/commands/service/windows.js.map +1 -0
- package/dist/cli/commands/service.d.ts +4 -0
- package/dist/cli/commands/service.d.ts.map +1 -0
- package/dist/cli/commands/service.js +68 -0
- package/dist/cli/commands/service.js.map +1 -0
- package/dist/cli/commands/session.d.ts +8 -0
- package/dist/cli/commands/session.d.ts.map +1 -0
- package/dist/cli/commands/session.js +270 -0
- package/dist/cli/commands/session.js.map +1 -0
- package/dist/cli/commands/transfer.d.ts +3 -0
- package/dist/cli/commands/transfer.d.ts.map +1 -0
- package/dist/cli/commands/transfer.js +57 -0
- package/dist/cli/commands/transfer.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +89 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/package-root.d.ts +27 -0
- package/dist/cli/package-root.d.ts.map +1 -0
- package/dist/cli/package-root.js +54 -0
- package/dist/cli/package-root.js.map +1 -0
- package/dist/cli/utils.d.ts +11 -0
- package/dist/cli/utils.d.ts.map +1 -0
- package/dist/cli/utils.js +48 -0
- package/dist/cli/utils.js.map +1 -0
- package/dist/daemon/config.d.ts +4 -0
- package/dist/daemon/config.d.ts.map +1 -0
- package/dist/daemon/config.js +58 -0
- package/dist/daemon/config.js.map +1 -0
- package/dist/daemon/hooks/permission-hook.mjs +108 -0
- package/dist/daemon/index.d.ts +3 -0
- package/dist/daemon/index.d.ts.map +1 -0
- package/dist/daemon/index.js +3 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/daemon/main.d.ts +3 -0
- package/dist/daemon/main.d.ts.map +1 -0
- package/dist/daemon/main.js +28 -0
- package/dist/daemon/main.js.map +1 -0
- package/dist/daemon/middleware/auth.d.ts +3 -0
- package/dist/daemon/middleware/auth.d.ts.map +1 -0
- package/dist/daemon/middleware/auth.js +22 -0
- package/dist/daemon/middleware/auth.js.map +1 -0
- package/dist/daemon/routes/ai-tasks.d.ts +4 -0
- package/dist/daemon/routes/ai-tasks.d.ts.map +1 -0
- package/dist/daemon/routes/ai-tasks.js +146 -0
- package/dist/daemon/routes/ai-tasks.js.map +1 -0
- package/dist/daemon/routes/context.d.ts +4 -0
- package/dist/daemon/routes/context.d.ts.map +1 -0
- package/dist/daemon/routes/context.js +49 -0
- package/dist/daemon/routes/context.js.map +1 -0
- package/dist/daemon/routes/execute.d.ts +4 -0
- package/dist/daemon/routes/execute.d.ts.map +1 -0
- package/dist/daemon/routes/execute.js +74 -0
- package/dist/daemon/routes/execute.js.map +1 -0
- package/dist/daemon/routes/health.d.ts +15 -0
- package/dist/daemon/routes/health.d.ts.map +1 -0
- package/dist/daemon/routes/health.js +38 -0
- package/dist/daemon/routes/health.js.map +1 -0
- package/dist/daemon/routes/pair.d.ts +11 -0
- package/dist/daemon/routes/pair.d.ts.map +1 -0
- package/dist/daemon/routes/pair.js +149 -0
- package/dist/daemon/routes/pair.js.map +1 -0
- package/dist/daemon/routes/peers.d.ts +5 -0
- package/dist/daemon/routes/peers.d.ts.map +1 -0
- package/dist/daemon/routes/peers.js +69 -0
- package/dist/daemon/routes/peers.js.map +1 -0
- package/dist/daemon/routes/transfer.d.ts +4 -0
- package/dist/daemon/routes/transfer.d.ts.map +1 -0
- package/dist/daemon/routes/transfer.js +135 -0
- package/dist/daemon/routes/transfer.js.map +1 -0
- package/dist/daemon/server.d.ts +8 -0
- package/dist/daemon/server.d.ts.map +1 -0
- package/dist/daemon/server.js +170 -0
- package/dist/daemon/server.js.map +1 -0
- package/dist/daemon/services/ai-task-manager.d.ts +56 -0
- package/dist/daemon/services/ai-task-manager.d.ts.map +1 -0
- package/dist/daemon/services/ai-task-manager.js +491 -0
- package/dist/daemon/services/ai-task-manager.js.map +1 -0
- package/dist/daemon/services/audit-logger.d.ts +16 -0
- package/dist/daemon/services/audit-logger.d.ts.map +1 -0
- package/dist/daemon/services/audit-logger.js +23 -0
- package/dist/daemon/services/audit-logger.js.map +1 -0
- package/dist/daemon/services/context-store.d.ts +17 -0
- package/dist/daemon/services/context-store.d.ts.map +1 -0
- package/dist/daemon/services/context-store.js +97 -0
- package/dist/daemon/services/context-store.js.map +1 -0
- package/dist/daemon/services/job-manager.d.ts +19 -0
- package/dist/daemon/services/job-manager.d.ts.map +1 -0
- package/dist/daemon/services/job-manager.js +92 -0
- package/dist/daemon/services/job-manager.js.map +1 -0
- package/dist/daemon/services/tls-manager.d.ts +33 -0
- package/dist/daemon/services/tls-manager.d.ts.map +1 -0
- package/dist/daemon/services/tls-manager.js +114 -0
- package/dist/daemon/services/tls-manager.js.map +1 -0
- package/dist/daemon/utils/which.d.ts +2 -0
- package/dist/daemon/utils/which.d.ts.map +1 -0
- package/dist/daemon/utils/which.js +18 -0
- package/dist/daemon/utils/which.js.map +1 -0
- package/dist/dashboard/config.d.ts +8 -0
- package/dist/dashboard/config.d.ts.map +1 -0
- package/dist/dashboard/config.js +22 -0
- package/dist/dashboard/config.js.map +1 -0
- package/dist/dashboard/public/app.js +120 -0
- package/dist/dashboard/public/icon-192.png +0 -0
- package/dist/dashboard/public/icon-512.png +0 -0
- package/dist/dashboard/public/index.html +85 -0
- package/dist/dashboard/public/manifest.json +12 -0
- package/dist/dashboard/public/style.css +784 -0
- package/dist/dashboard/public/sw.js +31 -0
- package/dist/dashboard/public/views/ai-tasks.js +679 -0
- package/dist/dashboard/public/views/context.js +167 -0
- package/dist/dashboard/public/views/messages.js +263 -0
- package/dist/dashboard/public/views/overview.js +228 -0
- package/dist/dashboard/public/views/peers.js +136 -0
- package/dist/dashboard/public/views/terminal.js +153 -0
- package/dist/dashboard/routes/ai-tasks.d.ts +3 -0
- package/dist/dashboard/routes/ai-tasks.d.ts.map +1 -0
- package/dist/dashboard/routes/ai-tasks.js +193 -0
- package/dist/dashboard/routes/ai-tasks.js.map +1 -0
- package/dist/dashboard/routes/messages.d.ts +3 -0
- package/dist/dashboard/routes/messages.d.ts.map +1 -0
- package/dist/dashboard/routes/messages.js +137 -0
- package/dist/dashboard/routes/messages.js.map +1 -0
- package/dist/dashboard/routes/peer-utils.d.ts +17 -0
- package/dist/dashboard/routes/peer-utils.d.ts.map +1 -0
- package/dist/dashboard/routes/peer-utils.js +193 -0
- package/dist/dashboard/routes/peer-utils.js.map +1 -0
- package/dist/dashboard/routes/peers-all.d.ts +3 -0
- package/dist/dashboard/routes/peers-all.d.ts.map +1 -0
- package/dist/dashboard/routes/peers-all.js +8 -0
- package/dist/dashboard/routes/peers-all.js.map +1 -0
- package/dist/dashboard/routes/proxy.d.ts +3 -0
- package/dist/dashboard/routes/proxy.d.ts.map +1 -0
- package/dist/dashboard/routes/proxy.js +59 -0
- package/dist/dashboard/routes/proxy.js.map +1 -0
- package/dist/dashboard/routes/sessions.d.ts +3 -0
- package/dist/dashboard/routes/sessions.d.ts.map +1 -0
- package/dist/dashboard/routes/sessions.js +64 -0
- package/dist/dashboard/routes/sessions.js.map +1 -0
- package/dist/dashboard/routes/sse.d.ts +3 -0
- package/dist/dashboard/routes/sse.d.ts.map +1 -0
- package/dist/dashboard/routes/sse.js +49 -0
- package/dist/dashboard/routes/sse.js.map +1 -0
- package/dist/dashboard/routes/status.d.ts +3 -0
- package/dist/dashboard/routes/status.d.ts.map +1 -0
- package/dist/dashboard/routes/status.js +38 -0
- package/dist/dashboard/routes/status.js.map +1 -0
- package/dist/dashboard/server.d.ts +3 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +77 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/dashboard/session-manager.d.ts +17 -0
- package/dist/dashboard/session-manager.d.ts.map +1 -0
- package/dist/dashboard/session-manager.js +225 -0
- package/dist/dashboard/session-manager.js.map +1 -0
- package/dist/discovery/health-checker.d.ts +15 -0
- package/dist/discovery/health-checker.d.ts.map +1 -0
- package/dist/discovery/health-checker.js +47 -0
- package/dist/discovery/health-checker.js.map +1 -0
- package/dist/discovery/index.d.ts +4 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +4 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/discovery/mdns.d.ts +21 -0
- package/dist/discovery/mdns.d.ts.map +1 -0
- package/dist/discovery/mdns.js +83 -0
- package/dist/discovery/mdns.js.map +1 -0
- package/dist/discovery/peer-registry.d.ts +18 -0
- package/dist/discovery/peer-registry.d.ts.map +1 -0
- package/dist/discovery/peer-registry.js +81 -0
- package/dist/discovery/peer-registry.js.map +1 -0
- package/dist/mcp-server/daemon-client.d.ts +69 -0
- package/dist/mcp-server/daemon-client.d.ts.map +1 -0
- package/dist/mcp-server/daemon-client.js +281 -0
- package/dist/mcp-server/daemon-client.js.map +1 -0
- package/dist/mcp-server/index.d.ts +3 -0
- package/dist/mcp-server/index.d.ts.map +1 -0
- package/dist/mcp-server/index.js +406 -0
- package/dist/mcp-server/index.js.map +1 -0
- package/dist/protocol/constants.d.ts +66 -0
- package/dist/protocol/constants.d.ts.map +1 -0
- package/dist/protocol/constants.js +66 -0
- package/dist/protocol/constants.js.map +1 -0
- package/dist/protocol/errors.d.ts +47 -0
- package/dist/protocol/errors.d.ts.map +1 -0
- package/dist/protocol/errors.js +62 -0
- package/dist/protocol/errors.js.map +1 -0
- package/dist/protocol/index.d.ts +5 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +5 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/protocol/schemas.d.ts +209 -0
- package/dist/protocol/schemas.d.ts.map +1 -0
- package/dist/protocol/schemas.js +115 -0
- package/dist/protocol/schemas.js.map +1 -0
- package/dist/protocol/types.d.ts +302 -0
- package/dist/protocol/types.d.ts.map +1 -0
- package/dist/protocol/types.js +2 -0
- package/dist/protocol/types.js.map +1 -0
- package/package.json +50 -0
- 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, '• $1');
|
|
625
|
+
// Numbered lists
|
|
626
|
+
html = html.replace(/^\d+\. (.+)$/gm, '· $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 });
|