@longshot/cli 0.0.1

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.
@@ -0,0 +1,386 @@
1
+ import { marked } from "marked";
2
+ import { layout } from "./layout.js";
3
+ function renderMessage(msg) {
4
+ const isUser = msg.role === "user";
5
+ const rendered = marked.parse(msg.content, { async: false });
6
+ return `<div class="message ${isUser ? "message-user" : "message-assistant"}">
7
+ <div class="message-role">${isUser ? "You" : "Claude"}</div>
8
+ <div class="message-content">${rendered}</div>
9
+ </div>`;
10
+ }
11
+ function typeIcon(type) {
12
+ switch (type) {
13
+ case "task_created": return "\u{1F4DD}";
14
+ case "task_complete": return "\u2705";
15
+ case "agent_spawn": return "\u{1F916}";
16
+ case "agent_complete": return "\u{1F3C1}";
17
+ case "checkpoint": return "\u{1F4BE}";
18
+ case "rejection": return "\u274C";
19
+ case "spec_update": return "\u{1F4C4}";
20
+ default: return "\u26A1";
21
+ }
22
+ }
23
+ function relativeTime(timestamp) {
24
+ const diff = Date.now() - new Date(timestamp).getTime();
25
+ const mins = Math.floor(diff / 60000);
26
+ if (mins < 1)
27
+ return "just now";
28
+ if (mins < 60)
29
+ return `${mins}m ago`;
30
+ const hours = Math.floor(mins / 60);
31
+ if (hours < 24)
32
+ return `${hours}h ago`;
33
+ const days = Math.floor(hours / 24);
34
+ return `${days}d ago`;
35
+ }
36
+ function renderActivityFeed(entries) {
37
+ if (entries.length === 0)
38
+ return "";
39
+ const items = entries.map((e) => {
40
+ const icon = typeIcon(e.type);
41
+ const time = relativeTime(e.timestamp);
42
+ let link = "";
43
+ const details = e.details || "";
44
+ // Extract taskId from details like "taskId:5" or summary like "Task #5"
45
+ const taskMatch = e.summary.match(/#(\d+)/) || details.match(/taskId:(\d+)/);
46
+ const runMatch = details.match(/runId:([\w-]+)/);
47
+ if (taskMatch) {
48
+ link = `/tasks/${taskMatch[1]}`;
49
+ }
50
+ if (runMatch) {
51
+ link = `/run/${runMatch[1]}`;
52
+ }
53
+ const summary = link
54
+ ? `<a href="${link}" class="activity-link">${e.summary}</a>`
55
+ : e.summary;
56
+ return `<div class="activity-item"><span class="activity-icon">${icon}</span><span class="activity-summary">${summary}</span><span class="activity-time">${time}</span></div>`;
57
+ }).join("\n");
58
+ return `<details class="activity-feed">
59
+ <summary class="activity-header">Recent Activity</summary>
60
+ <div class="activity-list">${items}</div>
61
+ </details>`;
62
+ }
63
+ export function chatPage(messages, _pendingDiffs, agentRunning, currentRunId, chatPending = false, queuedCount = 0, services = [], historyEntries = []) {
64
+ // Filter out agent messages (those with runId) — display-only filtering
65
+ const filteredMessages = messages.filter((m) => !m.runId);
66
+ const messageHtml = filteredMessages.map(renderMessage).join("\n");
67
+ const queueSuffix = queuedCount > 0 ? ` ${queuedCount} queued` : "";
68
+ const bannerHtml = agentRunning && currentRunId
69
+ ? `<a href="/agent/${currentRunId}" class="agent-banner">Agent working...${queueSuffix} tap to view progress</a>`
70
+ : "";
71
+ const thinkingHtml = chatPending
72
+ ? `<div class="message message-assistant" id="thinking-indicator">
73
+ <div class="message-role">Claude</div>
74
+ <div class="message-content"><em>Thinking...</em></div>
75
+ </div>`
76
+ : "";
77
+ const activityHtml = renderActivityFeed(historyEntries);
78
+ const body = `
79
+ <div class="chat-container">
80
+ ${bannerHtml}
81
+ ${activityHtml}
82
+ <div class="messages" id="messages">
83
+ ${messageHtml}
84
+ ${thinkingHtml}
85
+ </div>
86
+ ${services.length ? `<div class="service-status-bar" id="service-bar">
87
+ ${services.map((s) => {
88
+ const color = s.status === "running" ? "var(--accent-green)" : s.status === "crashed" ? "var(--accent-red)" : "var(--text-muted)";
89
+ return `<button class="service-toggle" data-id="${s.id}" data-status="${s.status}" onclick="toggleService('${s.id}', '${s.status}')">
90
+ <span class="service-dot" style="background:${color}"></span>
91
+ ${s.name}
92
+ </button>`;
93
+ }).join("\n")}
94
+ </div>` : ""}
95
+ <div class="chat-input-area">
96
+ <form class="chat-input" id="chat-form">
97
+ <textarea
98
+ name="message"
99
+ id="message-input"
100
+ placeholder="Ask Claude or describe what you want..."
101
+ rows="2"
102
+ autofocus
103
+ ></textarea>
104
+ <button type="submit" id="send-btn">Send</button>
105
+ </form>
106
+ <div class="chat-action-buttons">
107
+ <button class="draft-task-btn" id="draft-task-btn">Draft Task</button>
108
+ <button class="draft-task-btn" id="add-service-btn" style="border-color:var(--accent-green);color:var(--accent-green)">Add Service</button>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ <script>
113
+ const form = document.getElementById('chat-form');
114
+ const input = document.getElementById('message-input');
115
+ const messagesEl = document.getElementById('messages');
116
+ const sendBtn = document.getElementById('send-btn');
117
+ const draftBtn = document.getElementById('draft-task-btn');
118
+ let messageCount = ${filteredMessages.length};
119
+ let polling = ${chatPending ? "true" : "false"};
120
+
121
+ function scrollToBottom() {
122
+ messagesEl.scrollTop = messagesEl.scrollHeight;
123
+ }
124
+ scrollToBottom();
125
+
126
+ function escapeHtml(str) {
127
+ const div = document.createElement('div');
128
+ div.textContent = str;
129
+ return div.innerHTML;
130
+ }
131
+
132
+ input.addEventListener('input', () => {
133
+ input.style.height = 'auto';
134
+ input.style.height = Math.min(input.scrollHeight, 120) + 'px';
135
+ });
136
+
137
+ input.addEventListener('keydown', (e) => {
138
+ if (e.key === 'Enter' && !e.shiftKey) {
139
+ e.preventDefault();
140
+ form.dispatchEvent(new Event('submit'));
141
+ }
142
+ });
143
+
144
+ function showThinking() {
145
+ let indicator = document.getElementById('thinking-indicator');
146
+ if (!indicator) {
147
+ indicator = document.createElement('div');
148
+ indicator.id = 'thinking-indicator';
149
+ indicator.className = 'message message-assistant';
150
+ indicator.innerHTML = '<div class="message-role">Claude</div><div class="message-content"><em>Thinking...</em></div>';
151
+ messagesEl.appendChild(indicator);
152
+ }
153
+ scrollToBottom();
154
+ }
155
+
156
+ function removeThinking() {
157
+ const indicator = document.getElementById('thinking-indicator');
158
+ if (indicator) indicator.remove();
159
+ }
160
+
161
+ async function pollForResponse() {
162
+ if (polling) return; // already polling
163
+ polling = true;
164
+ showThinking();
165
+
166
+ const poll = async () => {
167
+ try {
168
+ const resp = await fetch('/api/chat/messages?after=' + messageCount);
169
+ const data = await resp.json();
170
+
171
+ if (data.messages.length > 0) {
172
+ removeThinking();
173
+ for (const msg of data.messages) {
174
+ const div = document.createElement('div');
175
+ div.className = 'message ' + (msg.role === 'user' ? 'message-user' : 'message-assistant');
176
+ div.innerHTML = '<div class="message-role">' + (msg.role === 'user' ? 'You' : 'Claude') + '</div><div class="message-content">' + (msg.html || msg.content) + '</div>';
177
+ messagesEl.appendChild(div);
178
+ }
179
+ messageCount = data.total;
180
+ scrollToBottom();
181
+ }
182
+
183
+ if (data.pending) {
184
+ showThinking();
185
+ setTimeout(poll, 2000);
186
+ } else {
187
+ removeThinking();
188
+ polling = false;
189
+ sendBtn.disabled = false;
190
+ sendBtn.textContent = 'Send';
191
+ refreshServiceBar();
192
+ }
193
+ } catch {
194
+ polling = false;
195
+ removeThinking();
196
+ sendBtn.disabled = false;
197
+ sendBtn.textContent = 'Send';
198
+ }
199
+ };
200
+
201
+ setTimeout(poll, 2000);
202
+ }
203
+
204
+ // If already pending on page load, start polling
205
+ if (polling) {
206
+ polling = false; // reset so pollForResponse can set it
207
+ pollForResponse();
208
+ }
209
+
210
+ form.addEventListener('submit', async (e) => {
211
+ e.preventDefault();
212
+ const message = input.value.trim();
213
+ if (!message || polling) return;
214
+
215
+ // Append user message to DOM immediately
216
+ const userDiv = document.createElement('div');
217
+ userDiv.className = 'message message-user';
218
+ userDiv.innerHTML = '<div class="message-role">You</div><div class="message-content">' + escapeHtml(message) + '</div>';
219
+ messagesEl.appendChild(userDiv);
220
+
221
+ input.value = '';
222
+ input.style.height = 'auto';
223
+ sendBtn.disabled = true;
224
+ sendBtn.textContent = '...';
225
+ scrollToBottom();
226
+
227
+ try {
228
+ const resp = await fetch('/api/chat/send', {
229
+ method: 'POST',
230
+ headers: { 'Content-Type': 'application/json' },
231
+ body: JSON.stringify({ message }),
232
+ });
233
+ const data = await resp.json();
234
+
235
+ if (resp.ok) {
236
+ messageCount++; // user message was saved server-side
237
+ pollForResponse();
238
+ } else {
239
+ sendBtn.disabled = false;
240
+ sendBtn.textContent = 'Send';
241
+ if (data.error) {
242
+ const errDiv = document.createElement('div');
243
+ errDiv.className = 'error';
244
+ errDiv.textContent = data.error;
245
+ messagesEl.appendChild(errDiv);
246
+ }
247
+ }
248
+ } catch (err) {
249
+ sendBtn.disabled = false;
250
+ sendBtn.textContent = 'Send';
251
+ const errDiv = document.createElement('div');
252
+ errDiv.className = 'error';
253
+ errDiv.textContent = 'Failed to send message';
254
+ messagesEl.appendChild(errDiv);
255
+ }
256
+
257
+ input.focus();
258
+ });
259
+
260
+ // Service controls
261
+ async function toggleService(id, status) {
262
+ const action = status === 'running' ? 'stop' : 'start';
263
+ await fetch('/api/services/' + id + '/' + action, { method: 'POST' });
264
+ // Refresh service bar
265
+ refreshServiceBar();
266
+ }
267
+
268
+ async function refreshServiceBar() {
269
+ try {
270
+ const resp = await fetch('/api/services');
271
+ const services = await resp.json();
272
+ const bar = document.getElementById('service-bar');
273
+ if (!bar && services.length === 0) return;
274
+ if (!bar && services.length > 0) {
275
+ location.reload();
276
+ return;
277
+ }
278
+ if (bar) {
279
+ bar.innerHTML = services.map(s => {
280
+ const color = s.status === 'running' ? 'var(--accent-green)' : s.status === 'crashed' ? 'var(--accent-red)' : 'var(--text-muted)';
281
+ return '<button class="service-toggle" data-id="' + s.id + '" data-status="' + s.status + '" onclick="toggleService(\\'' + s.id + '\\', \\'' + s.status + '\\')">' +
282
+ '<span class="service-dot" style="background:' + color + '"></span>' + s.name + '</button>';
283
+ }).join('');
284
+ }
285
+ } catch {}
286
+ }
287
+
288
+ // Poll service status
289
+ setInterval(refreshServiceBar, 5000);
290
+
291
+ const addServiceBtn = document.getElementById('add-service-btn');
292
+ addServiceBtn.addEventListener('click', async () => {
293
+ const message = input.value.trim();
294
+ if (!message || polling) return;
295
+
296
+ // Show user message in DOM immediately
297
+ const userDiv = document.createElement('div');
298
+ userDiv.className = 'message message-user';
299
+ userDiv.innerHTML = '<div class="message-role">You</div><div class="message-content">' + escapeHtml(message) + '</div>';
300
+ messagesEl.appendChild(userDiv);
301
+
302
+ input.value = '';
303
+ input.style.height = 'auto';
304
+ sendBtn.disabled = true;
305
+ sendBtn.textContent = '...';
306
+ scrollToBottom();
307
+
308
+ try {
309
+ const resp = await fetch('/api/chat/add-service', {
310
+ method: 'POST',
311
+ headers: { 'Content-Type': 'application/json' },
312
+ body: JSON.stringify({ message }),
313
+ });
314
+ const data = await resp.json();
315
+
316
+ if (resp.ok) {
317
+ messageCount++;
318
+ pollForResponse();
319
+ } else {
320
+ sendBtn.disabled = false;
321
+ sendBtn.textContent = 'Send';
322
+ if (data.error) {
323
+ const errDiv = document.createElement('div');
324
+ errDiv.className = 'error';
325
+ errDiv.textContent = data.error;
326
+ messagesEl.appendChild(errDiv);
327
+ }
328
+ }
329
+ } catch (err) {
330
+ sendBtn.disabled = false;
331
+ sendBtn.textContent = 'Send';
332
+ const errDiv = document.createElement('div');
333
+ errDiv.className = 'error';
334
+ errDiv.textContent = 'Failed to send message';
335
+ messagesEl.appendChild(errDiv);
336
+ }
337
+
338
+ input.focus();
339
+ });
340
+
341
+ // Make functions available globally for onclick handlers
342
+ window.toggleService = toggleService;
343
+
344
+ draftBtn.addEventListener('click', async () => {
345
+ draftBtn.disabled = true;
346
+ draftBtn.textContent = 'Queuing...';
347
+ try {
348
+ const message = input.value.trim();
349
+ if (!message) {
350
+ draftBtn.disabled = false;
351
+ draftBtn.textContent = 'Draft Task';
352
+ return;
353
+ }
354
+ // Save to chat history and show in DOM
355
+ const userDiv = document.createElement('div');
356
+ userDiv.className = 'message message-user';
357
+ userDiv.innerHTML = '<div class="message-role">You</div><div class="message-content">' + escapeHtml(message) + '</div>';
358
+ messagesEl.appendChild(userDiv);
359
+ input.value = '';
360
+ input.style.height = 'auto';
361
+ await fetch('/chat', {
362
+ method: 'POST',
363
+ headers: { 'Content-Type': 'application/json' },
364
+ body: JSON.stringify({ message }),
365
+ });
366
+ // Enqueue draft task
367
+ const resp = await fetch('/agent/draft-task', {
368
+ method: 'POST',
369
+ headers: { 'Content-Type': 'application/json' },
370
+ body: JSON.stringify({ message }),
371
+ });
372
+ const data = await resp.json();
373
+ if (data.ok) {
374
+ window.location.href = '/tasks';
375
+ } else {
376
+ draftBtn.disabled = false;
377
+ draftBtn.textContent = 'Draft Task';
378
+ }
379
+ } catch (err) {
380
+ draftBtn.disabled = false;
381
+ draftBtn.textContent = 'Draft Task';
382
+ }
383
+ });
384
+ </script>`;
385
+ return layout("Chat", "chat", body);
386
+ }