@farazirfan/costar-server-executor 1.7.4 → 1.7.7

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/public/index.html CHANGED
@@ -509,6 +509,146 @@
509
509
  flex: 1;
510
510
  }
511
511
 
512
+ /* ─── Chat Tool Cards ──────────────────────────── */
513
+ .tool-card {
514
+ background: var(--bg-secondary);
515
+ border: 1px solid var(--border);
516
+ border-radius: var(--radius);
517
+ margin: 8px 0;
518
+ font-size: 13px;
519
+ overflow: hidden;
520
+ }
521
+
522
+ .tool-card-header {
523
+ display: flex;
524
+ align-items: center;
525
+ gap: 8px;
526
+ padding: 8px 12px;
527
+ cursor: pointer;
528
+ user-select: none;
529
+ transition: background 0.15s;
530
+ }
531
+
532
+ .tool-card-header:hover {
533
+ background: var(--bg-hover);
534
+ }
535
+
536
+ .tool-icon {
537
+ font-size: 14px;
538
+ flex-shrink: 0;
539
+ }
540
+
541
+ .tool-icon.running {
542
+ animation: spin 1s linear infinite;
543
+ }
544
+
545
+ @keyframes spin {
546
+ from { transform: rotate(0deg); }
547
+ to { transform: rotate(360deg); }
548
+ }
549
+
550
+ .tool-name {
551
+ font-weight: 600;
552
+ color: var(--accent);
553
+ font-family: var(--mono);
554
+ font-size: 12px;
555
+ }
556
+
557
+ .tool-status {
558
+ margin-left: auto;
559
+ font-size: 11px;
560
+ font-family: var(--mono);
561
+ color: var(--text-muted);
562
+ }
563
+
564
+ .tool-status.success {
565
+ color: var(--green);
566
+ }
567
+
568
+ .tool-status.error {
569
+ color: var(--red);
570
+ }
571
+
572
+ .tool-chevron {
573
+ font-size: 10px;
574
+ color: var(--text-muted);
575
+ transition: transform 0.2s;
576
+ }
577
+
578
+ .tool-card.expanded .tool-chevron {
579
+ transform: rotate(90deg);
580
+ }
581
+
582
+ .tool-card-body {
583
+ display: none;
584
+ border-top: 1px solid var(--border);
585
+ }
586
+
587
+ .tool-card.expanded .tool-card-body {
588
+ display: block;
589
+ }
590
+
591
+ .tool-section {
592
+ padding: 8px 12px;
593
+ }
594
+
595
+ .tool-section + .tool-section {
596
+ border-top: 1px solid var(--border);
597
+ }
598
+
599
+ .tool-section-label {
600
+ font-size: 10px;
601
+ font-weight: 600;
602
+ text-transform: uppercase;
603
+ letter-spacing: 0.05em;
604
+ color: var(--text-muted);
605
+ margin-bottom: 4px;
606
+ }
607
+
608
+ .tool-section pre {
609
+ font-family: var(--mono);
610
+ font-size: 11px;
611
+ color: var(--text-secondary);
612
+ white-space: pre-wrap;
613
+ word-break: break-all;
614
+ max-height: 200px;
615
+ overflow-y: auto;
616
+ margin: 0;
617
+ line-height: 1.5;
618
+ }
619
+
620
+ /* Streaming cursor */
621
+ .streaming-cursor::after {
622
+ content: "";
623
+ display: inline-block;
624
+ width: 8px;
625
+ height: 16px;
626
+ background: var(--accent);
627
+ margin-left: 2px;
628
+ animation: blink 0.8s step-end infinite;
629
+ vertical-align: text-bottom;
630
+ }
631
+
632
+ @keyframes blink {
633
+ 50% { opacity: 0; }
634
+ }
635
+
636
+ /* Chat metadata bar */
637
+ .chat-meta {
638
+ display: flex;
639
+ gap: 12px;
640
+ padding: 6px 0;
641
+ font-size: 11px;
642
+ color: var(--text-muted);
643
+ font-family: var(--mono);
644
+ }
645
+
646
+ .chat-meta span {
647
+ display: flex;
648
+ align-items: center;
649
+ gap: 4px;
650
+ }
651
+
512
652
  /* ─── Logs ─────────────────────────────────────── */
513
653
  .log-toolbar {
514
654
  display: flex;
@@ -1253,7 +1393,9 @@
1253
1393
  return `${m}m`;
1254
1394
  }
1255
1395
 
1256
- // ─── Chat ────────────────────────────────────────
1396
+ // ─── Chat (Real-time Streaming) ─────────────────
1397
+ let chatStreaming = false;
1398
+
1257
1399
  async function initChat() {
1258
1400
  if (!chatSessionId) {
1259
1401
  try {
@@ -1268,7 +1410,7 @@
1268
1410
  async function sendMessage() {
1269
1411
  const input = document.getElementById('chat-input');
1270
1412
  const msg = input.value.trim();
1271
- if (!msg) return;
1413
+ if (!msg || chatStreaming) return;
1272
1414
 
1273
1415
  if (!chatSessionId) await initChat();
1274
1416
 
@@ -1280,41 +1422,265 @@
1280
1422
  if (emptyState) emptyState.remove();
1281
1423
 
1282
1424
  // Add user message
1283
- messagesEl.innerHTML += chatMsgHTML('user', msg);
1284
- messagesEl.scrollTop = messagesEl.scrollHeight;
1285
-
1286
- // Add loading
1287
- const loadingId = 'loading-' + Date.now();
1288
- messagesEl.innerHTML += `<div id="${loadingId}" class="chat-msg assistant"><div class="avatar">C</div><div class="msg-content"><div class="msg-role">CoStar</div><div class="spinner"></div></div></div>`;
1289
- messagesEl.scrollTop = messagesEl.scrollHeight;
1425
+ messagesEl.insertAdjacentHTML('beforeend', chatMsgHTML('user', msg));
1426
+ scrollChat();
1427
+
1428
+ // Create streaming assistant message container
1429
+ const streamId = 'stream-' + Date.now();
1430
+ messagesEl.insertAdjacentHTML('beforeend', `
1431
+ <div id="${streamId}" class="chat-msg assistant">
1432
+ <div class="avatar">C</div>
1433
+ <div class="msg-content">
1434
+ <div class="msg-role">CoStar</div>
1435
+ <div id="${streamId}-tools"></div>
1436
+ <div id="${streamId}-text" class="streaming-cursor"></div>
1437
+ <div id="${streamId}-meta" class="chat-meta" style="display:none;"></div>
1438
+ </div>
1439
+ </div>
1440
+ `);
1441
+ scrollChat();
1290
1442
 
1291
1443
  // Disable send
1292
1444
  const sendBtn = document.getElementById('chat-send-btn');
1293
1445
  sendBtn.disabled = true;
1294
- sendBtn.textContent = '...';
1446
+ sendBtn.textContent = 'Thinking...';
1447
+ chatStreaming = true;
1295
1448
 
1296
1449
  try {
1297
- const res = await api(`/api/sessions/${chatSessionId}/messages`, {
1298
- method: 'POST',
1299
- body: JSON.stringify({ message: msg }),
1300
- });
1301
-
1302
- document.getElementById(loadingId).remove();
1303
- messagesEl.innerHTML += chatMsgHTML('assistant', res.content);
1304
- messagesEl.scrollTop = messagesEl.scrollHeight;
1450
+ await streamChat(chatSessionId, msg, streamId);
1305
1451
  } catch (err) {
1306
- document.getElementById(loadingId).remove();
1307
- messagesEl.innerHTML += chatMsgHTML('assistant', `Error: ${err.message}`);
1452
+ const textEl = document.getElementById(`${streamId}-text`);
1453
+ if (textEl) {
1454
+ textEl.classList.remove('streaming-cursor');
1455
+ textEl.innerHTML = `<span style="color:var(--red);">Error: ${esc(err.message)}</span>`;
1456
+ }
1308
1457
  } finally {
1309
1458
  sendBtn.disabled = false;
1310
1459
  sendBtn.textContent = 'Send';
1460
+ chatStreaming = false;
1461
+ // Remove streaming cursor
1462
+ const textEl = document.getElementById(`${streamId}-text`);
1463
+ if (textEl) textEl.classList.remove('streaming-cursor');
1311
1464
  }
1312
1465
  }
1313
1466
 
1467
+ function streamChat(sessionId, message, streamId) {
1468
+ return new Promise((resolve, reject) => {
1469
+ const textEl = document.getElementById(`${streamId}-text`);
1470
+ const toolsEl = document.getElementById(`${streamId}-tools`);
1471
+ const metaEl = document.getElementById(`${streamId}-meta`);
1472
+ let fullText = '';
1473
+ let toolCount = 0;
1474
+
1475
+ // Use fetch + ReadableStream for POST-based SSE
1476
+ fetch(`/api/sessions/${sessionId}/messages/stream`, {
1477
+ method: 'POST',
1478
+ headers: { 'Content-Type': 'application/json' },
1479
+ body: JSON.stringify({ message }),
1480
+ }).then(response => {
1481
+ if (!response.ok) {
1482
+ throw new Error(`HTTP ${response.status}`);
1483
+ }
1484
+
1485
+ const reader = response.body.getReader();
1486
+ const decoder = new TextDecoder();
1487
+ let buffer = '';
1488
+
1489
+ function processChunk(chunk) {
1490
+ buffer += chunk;
1491
+ const lines = buffer.split('\n');
1492
+ buffer = lines.pop() || '';
1493
+
1494
+ let eventType = '';
1495
+ let eventData = '';
1496
+
1497
+ for (const line of lines) {
1498
+ if (line.startsWith('event: ')) {
1499
+ eventType = line.slice(7).trim();
1500
+ } else if (line.startsWith('data: ')) {
1501
+ eventData = line.slice(6);
1502
+ if (eventType && eventData) {
1503
+ handleSSEEvent(eventType, eventData, textEl, toolsEl, metaEl, streamId, {
1504
+ fullText: () => fullText,
1505
+ setFullText: (t) => { fullText = t; },
1506
+ toolCount: () => toolCount,
1507
+ incToolCount: () => { toolCount++; },
1508
+ });
1509
+ eventType = '';
1510
+ eventData = '';
1511
+ }
1512
+ } else if (line === '') {
1513
+ eventType = '';
1514
+ eventData = '';
1515
+ }
1516
+ }
1517
+ }
1518
+
1519
+ function pump() {
1520
+ return reader.read().then(({ done, value }) => {
1521
+ if (done) {
1522
+ if (buffer.trim()) processChunk('\n');
1523
+ resolve();
1524
+ return;
1525
+ }
1526
+ processChunk(decoder.decode(value, { stream: true }));
1527
+ return pump();
1528
+ });
1529
+ }
1530
+
1531
+ return pump();
1532
+ }).catch(reject);
1533
+ });
1534
+ }
1535
+
1536
+ function handleSSEEvent(type, dataStr, textEl, toolsEl, metaEl, streamId, state) {
1537
+ let data;
1538
+ try { data = JSON.parse(dataStr); } catch { return; }
1539
+
1540
+ switch (type) {
1541
+ case 'connected':
1542
+ break;
1543
+
1544
+ case 'text_delta':
1545
+ if (data.text && textEl) {
1546
+ state.setFullText(state.fullText() + data.text);
1547
+ textEl.innerHTML = formatMessage(state.fullText());
1548
+ scrollChat();
1549
+ }
1550
+ break;
1551
+
1552
+ case 'tool_start':
1553
+ if (toolsEl) {
1554
+ state.incToolCount();
1555
+ const cardId = `${streamId}-tool-${data.toolCallId || state.toolCount()}`;
1556
+ const argsStr = data.args ? JSON.stringify(data.args, null, 2) : '{}';
1557
+ const shortArgs = argsStr.length > 100 ? argsStr.slice(0, 100) + '...' : argsStr;
1558
+
1559
+ toolsEl.insertAdjacentHTML('beforeend', `
1560
+ <div class="tool-card" id="${cardId}" onclick="toggleToolCard('${cardId}')">
1561
+ <div class="tool-card-header">
1562
+ <span class="tool-icon running">&#9881;</span>
1563
+ <span class="tool-name">${esc(data.name || 'tool')}</span>
1564
+ <span class="tool-status">running...</span>
1565
+ <span class="tool-chevron">&#9654;</span>
1566
+ </div>
1567
+ <div class="tool-card-body">
1568
+ <div class="tool-section">
1569
+ <div class="tool-section-label">Arguments</div>
1570
+ <pre>${esc(argsStr)}</pre>
1571
+ </div>
1572
+ <div class="tool-section" id="${cardId}-result" style="display:none;">
1573
+ <div class="tool-section-label">Result</div>
1574
+ <pre></pre>
1575
+ </div>
1576
+ </div>
1577
+ </div>
1578
+ `);
1579
+ scrollChat();
1580
+
1581
+ // Update send button with tool count
1582
+ const sendBtn = document.getElementById('chat-send-btn');
1583
+ sendBtn.textContent = `Tool ${state.toolCount()}...`;
1584
+ }
1585
+ break;
1586
+
1587
+ case 'tool_end':
1588
+ if (toolsEl) {
1589
+ const cardId = `${streamId}-tool-${data.toolCallId || state.toolCount()}`;
1590
+ const card = document.getElementById(cardId);
1591
+ if (card) {
1592
+ // Update icon — stop spinning
1593
+ const icon = card.querySelector('.tool-icon');
1594
+ if (icon) {
1595
+ icon.classList.remove('running');
1596
+ icon.innerHTML = data.isError ? '&#10060;' : '&#9989;';
1597
+ }
1598
+
1599
+ // Update status
1600
+ const status = card.querySelector('.tool-status');
1601
+ if (status) {
1602
+ const durationStr = data.durationMs ? ` (${formatDurationShort(data.durationMs)})` : '';
1603
+ status.textContent = data.isError ? `error${durationStr}` : `done${durationStr}`;
1604
+ status.className = `tool-status ${data.isError ? 'error' : 'success'}`;
1605
+ }
1606
+
1607
+ // Show result
1608
+ const resultSection = document.getElementById(`${cardId}-result`);
1609
+ if (resultSection && data.result) {
1610
+ resultSection.style.display = '';
1611
+ resultSection.querySelector('pre').textContent = data.result;
1612
+ }
1613
+ }
1614
+ scrollChat();
1615
+ }
1616
+ break;
1617
+
1618
+ case 'done':
1619
+ if (textEl) {
1620
+ textEl.classList.remove('streaming-cursor');
1621
+ // Ensure final text is rendered
1622
+ if (data.response) {
1623
+ textEl.innerHTML = formatMessage(data.response);
1624
+ }
1625
+ }
1626
+ if (metaEl && (data.toolCalls > 0 || data.inputTokens > 0)) {
1627
+ metaEl.style.display = '';
1628
+ let metaHTML = '';
1629
+ if (data.toolCalls > 0) metaHTML += `<span>&#128295; ${data.toolCalls} tool${data.toolCalls > 1 ? 's' : ''}</span>`;
1630
+ if (data.inputTokens > 0) metaHTML += `<span>&#8594; ${data.inputTokens.toLocaleString()} in</span>`;
1631
+ if (data.outputTokens > 0) metaHTML += `<span>&#8592; ${data.outputTokens.toLocaleString()} out</span>`;
1632
+ metaEl.innerHTML = metaHTML;
1633
+ }
1634
+ break;
1635
+
1636
+ case 'error':
1637
+ if (textEl) {
1638
+ textEl.classList.remove('streaming-cursor');
1639
+ const errorMsg = data.message || 'Unknown error';
1640
+ const currentText = state.fullText();
1641
+ textEl.innerHTML = (currentText ? formatMessage(currentText) + '<br><br>' : '') +
1642
+ `<span style="color:var(--red);">Error: ${esc(errorMsg)}</span>`;
1643
+ }
1644
+ break;
1645
+ }
1646
+ }
1647
+
1648
+ function toggleToolCard(cardId) {
1649
+ const card = document.getElementById(cardId);
1650
+ if (card) card.classList.toggle('expanded');
1651
+ }
1652
+
1653
+ function formatDurationShort(ms) {
1654
+ if (ms < 1000) return `${ms}ms`;
1655
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
1656
+ return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
1657
+ }
1658
+
1659
+ function formatMessage(text) {
1660
+ if (!text) return '';
1661
+ // Simple markdown-ish formatting
1662
+ let html = esc(text);
1663
+ // Code blocks: ```...```
1664
+ html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre style="background:var(--bg-primary);border:1px solid var(--border);border-radius:var(--radius);padding:10px;margin:8px 0;overflow-x:auto;font-family:var(--mono);font-size:12px;">$2</pre>');
1665
+ // Inline code: `...`
1666
+ html = html.replace(/`([^`]+)`/g, '<code style="background:var(--bg-primary);padding:2px 6px;border-radius:4px;font-family:var(--mono);font-size:12px;">$1</code>');
1667
+ // Bold: **...**
1668
+ html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
1669
+ // Newlines
1670
+ html = html.replace(/\n/g, '<br>');
1671
+ return html;
1672
+ }
1673
+
1674
+ function scrollChat() {
1675
+ const el = document.getElementById('chat-messages');
1676
+ if (el) el.scrollTop = el.scrollHeight;
1677
+ }
1678
+
1314
1679
  function chatMsgHTML(role, content) {
1315
1680
  const avatar = role === 'user' ? 'U' : 'C';
1316
1681
  const label = role === 'user' ? 'You' : 'CoStar';
1317
- return `<div class="chat-msg ${role}"><div class="avatar">${avatar}</div><div class="msg-content"><div class="msg-role">${label}</div><div>${esc(content)}</div></div></div>`;
1682
+ const formattedContent = role === 'user' ? esc(content) : formatMessage(content);
1683
+ return `<div class="chat-msg ${role}"><div class="avatar">${avatar}</div><div class="msg-content"><div class="msg-role">${label}</div><div>${formattedContent}</div></div></div>`;
1318
1684
  }
1319
1685
 
1320
1686
  // ─── Cron ────────────────────────────────────────