Skip to content

Commit b8185ca

Browse files
shreyas-lyzrclaude
andcommitted
feat: add unified Logs tab to web UI for real-time log viewing
Adds a new Logs tab that captures all server-side logs (voice, telegram, whatsapp, triggers, sdk, scheduler) via console intercept and streams them to the browser in real time over WebSocket. Includes source/level filtering, text search, auto-scroll, and a REST endpoint for history. Bumps version to 1.4.0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8e755a9 commit b8185ca

4 files changed

Lines changed: 238 additions & 3 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "gitclaw",
3-
"version": "1.3.3",
3+
"version": "1.4.0",
44
"description": "A universal git-native multimodal always learning AI Agent (TinyHuman)",
55
"author": "shreyaskapale",
66
"license": "MIT",

src/voice/adapter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export interface ServerError { type: "error"; message: string; }
1919
export interface ServerInterrupt { type: "interrupt"; }
2020
export interface ServerFilesChanged { type: "files_changed"; }
2121
export interface ServerMemorySaving { type: "memory_saving"; status: "start" | "done"; text?: string; }
22-
export type ServerMessage = ServerAudioDelta | ServerTranscript | ServerAgentWorking | ServerAgentDone | ServerToolCall | ServerToolResult | ServerAgentThinking | ServerError | ServerInterrupt | ServerFilesChanged | ServerMemorySaving;
22+
export interface ServerLogEntry { type: "log_entry"; entry: { id: number; ts: string; source: string; level: string; message: string }; }
23+
export type ServerMessage = ServerAudioDelta | ServerTranscript | ServerAgentWorking | ServerAgentDone | ServerToolCall | ServerToolResult | ServerAgentThinking | ServerError | ServerInterrupt | ServerFilesChanged | ServerMemorySaving | ServerLogEntry;
2324

2425
// Adapter interface — adapters receive ClientMessages, emit ServerMessages
2526
export interface MultimodalAdapter {

src/voice/server.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,64 @@ import cron from "node-cron";
2222
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
2323
const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
2424

25+
// ── Log ring buffer for Logs UI ───────────────────────────────────────
26+
interface LogEntry {
27+
id: number;
28+
ts: string;
29+
source: string;
30+
level: "info" | "warn" | "error";
31+
message: string;
32+
}
33+
34+
class LogRingBuffer {
35+
private buf: LogEntry[] = [];
36+
private nextId = 1;
37+
private cap: number;
38+
constructor(capacity = 2000) { this.cap = capacity; }
39+
push(source: string, level: "info" | "warn" | "error", message: string): LogEntry {
40+
const entry: LogEntry = { id: this.nextId++, ts: new Date().toISOString(), source, level, message };
41+
this.buf.push(entry);
42+
if (this.buf.length > this.cap) this.buf.shift();
43+
return entry;
44+
}
45+
all(): LogEntry[] { return this.buf.slice(); }
46+
since(id: number): LogEntry[] { return this.buf.filter(e => e.id > id); }
47+
}
48+
49+
const logBuffer = new LogRingBuffer(2000);
50+
let logBroadcast: ((entry: LogEntry) => void) | null = null;
51+
52+
function stripAnsi(s: string): string { return s.replace(/\x1b\[\d*m/g, ""); }
53+
function extractSource(msg: string): { source: string; cleaned: string } {
54+
const m = msg.match(/^\[(\w+(?:\/\w+)?)\]\s*/);
55+
if (m) return { source: m[1].split("/")[0].toLowerCase(), cleaned: msg.slice(m[0].length) };
56+
return { source: "system", cleaned: msg };
57+
}
58+
59+
function installConsoleIntercept() {
60+
const origLog = console.log.bind(console);
61+
const origError = console.error.bind(console);
62+
const origWarn = console.warn.bind(console);
63+
64+
function intercept(level: "info" | "warn" | "error", origFn: (...args: any[]) => void, ...args: any[]) {
65+
origFn(...args);
66+
try {
67+
const raw = args.map(a => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
68+
const clean = stripAnsi(raw);
69+
if (!clean.trim()) return;
70+
const { source, cleaned } = extractSource(clean);
71+
const entry = logBuffer.push(source, level, cleaned);
72+
if (logBroadcast) logBroadcast(entry);
73+
} catch { /* non-fatal */ }
74+
}
75+
76+
console.log = (...args: any[]) => intercept("info", origLog, ...args);
77+
console.error = (...args: any[]) => intercept("error", origError, ...args);
78+
console.warn = (...args: any[]) => intercept("warn", origWarn, ...args);
79+
}
80+
81+
installConsoleIntercept();
82+
2583
// ── Background memory saver ────────────────────────────────────────────
2684
// Patterns that indicate the user is sharing personal info worth saving.
2785
// This runs server-side so we don't depend on the voice LLM deciding to save.
@@ -704,6 +762,9 @@ ${runningContext}`;
704762
}
705763
}
706764

765+
// Wire log broadcast to WebSocket
766+
logBroadcast = (entry) => broadcastToBrowsers({ type: "log_entry", entry } as ServerMessage);
767+
707768
// ── Scheduler setup ────────────────────────────────────────────────
708769
const scheduleSendToBrowser = (msg: ServerMessage) => {
709770
broadcastToBrowsers(msg);
@@ -2352,6 +2413,18 @@ return false;
23522413
jsonReply(res, 200, { entries: [] });
23532414
}
23542415

2416+
// ── Logs API ────────────────────────────────────────────────────
2417+
} else if (url.pathname === "/api/logs" && req.method === "GET") {
2418+
const sinceParam = url.searchParams.get("since");
2419+
const sourceFilter = url.searchParams.get("source") || "";
2420+
const levelFilter = url.searchParams.get("level") || "";
2421+
const searchFilter = (url.searchParams.get("q") || "").toLowerCase();
2422+
let entries = sinceParam ? logBuffer.since(parseInt(sinceParam, 10)) : logBuffer.all();
2423+
if (sourceFilter) entries = entries.filter(e => e.source === sourceFilter);
2424+
if (levelFilter) entries = entries.filter(e => e.level === levelFilter);
2425+
if (searchFilter) entries = entries.filter(e => e.message.toLowerCase().includes(searchFilter));
2426+
jsonReply(res, 200, { entries });
2427+
23552428
// ── Skills Marketplace proxy ────────────────────────────────────
23562429
} else if (url.pathname === "/api/skills-mp/proxy" && req.method === "GET") {
23572430
const proxyPath = url.searchParams.get("path") || "/";

src/voice/ui.html

Lines changed: 162 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,51 @@
854854
.flows-saved-item .flow-delete:hover { opacity: 1; }
855855

856856
/* Scheduler view */
857+
/* Logs view */
858+
.logs-view { display: flex; flex-direction: column; flex: 1; overflow: hidden; }
859+
.logs-view.hidden { display: none; }
860+
.logs-toolbar {
861+
display: flex; align-items: center; justify-content: space-between; gap: 8px;
862+
padding: 8px 12px; background: #161b22; border-bottom: 1px solid #21262d; flex-shrink: 0;
863+
}
864+
.logs-filters { display: flex; gap: 6px; align-items: center; flex: 1; min-width: 0; }
865+
.logs-filters select, .logs-filters input {
866+
padding: 5px 8px; background: #0d1117; border: 1px solid #30363d; border-radius: 4px;
867+
color: #c9d1d9; font-size: 11px; font-family: inherit; outline: none;
868+
}
869+
.logs-filters select { min-width: 100px; }
870+
.logs-filters input { flex: 1; min-width: 120px; }
871+
.logs-filters select:focus, .logs-filters input:focus { border-color: #58a6ff; }
872+
.logs-actions { display: flex; gap: 8px; align-items: center; flex-shrink: 0; }
873+
.logs-actions label { font-size: 11px; color: #8b949e; display: flex; align-items: center; gap: 4px; cursor: pointer; }
874+
.logs-actions button {
875+
padding: 4px 10px; background: #21262d; border: 1px solid #30363d; border-radius: 4px;
876+
color: #c9d1d9; font-size: 11px; cursor: pointer; font-family: inherit;
877+
}
878+
.logs-actions button:hover { background: #30363d; }
879+
.logs-output {
880+
flex: 1; overflow-y: auto; padding: 0; font-family: 'IBM Plex Mono', monospace;
881+
font-size: 12px; line-height: 1.6; background: #0d1117;
882+
}
883+
.log-entry { display: flex; gap: 8px; padding: 1px 10px; border-bottom: 1px solid #161b2200; }
884+
.log-entry:hover { background: #161b22; }
885+
.log-ts { color: #484f58; min-width: 80px; flex-shrink: 0; white-space: nowrap; }
886+
.log-src {
887+
display: inline-block; min-width: 72px; text-align: center; font-size: 10px; font-weight: 600;
888+
padding: 0 6px; border-radius: 3px; flex-shrink: 0; line-height: 1.6;
889+
}
890+
.log-src-voice { color: #58a6ff; background: rgba(88,166,255,0.1); }
891+
.log-src-telegram { color: #2188ff; background: rgba(33,136,255,0.1); }
892+
.log-src-whatsapp { color: #3fb950; background: rgba(63,185,80,0.1); }
893+
.log-src-triggers { color: #d29922; background: rgba(210,153,34,0.1); }
894+
.log-src-sdk { color: #bc8cff; background: rgba(188,140,255,0.1); }
895+
.log-src-scheduler { color: #f0883e; background: rgba(240,136,62,0.1); }
896+
.log-src-system { color: #8b949e; background: rgba(139,148,158,0.1); }
897+
.log-msg { flex: 1; white-space: pre-wrap; word-break: break-all; }
898+
.log-msg.log-error { color: #f85149; }
899+
.log-msg.log-warn { color: #d29922; }
900+
.log-msg.log-info { color: #c9d1d9; }
901+
857902
.scheduler-view { display: flex; flex-direction: column; flex: 1; overflow: hidden; }
858903
.scheduler-view.hidden { display: none; }
859904
.scheduler-content { flex: 1; overflow-y: auto; padding: 16px 20px; display: flex; flex-direction: column; gap: 16px; }
@@ -1043,6 +1088,10 @@
10431088
.comms-view { flex-direction: column; overflow-y: auto; }
10441089
.comms-view > div { width: 100% !important; min-width: 0 !important; max-width: none !important; border-right: none !important; }
10451090

1091+
/* ── Logs — stack toolbar ── */
1092+
.logs-toolbar { flex-direction: column; gap: 6px; }
1093+
.logs-filters { flex-wrap: wrap; }
1094+
10461095
/* ── Settings — already responsive via max-width ── */
10471096
.settings-view { padding: 12px; }
10481097
}
@@ -1064,6 +1113,7 @@ <h1>Gitclaw: {{AGENT_NAME}}</h1>
10641113
<button class="view-tab" id="tabComms" onclick="switchView('comms')">Communication</button>
10651114
<button class="view-tab" id="tabFlows" onclick="switchView('flows')">SkillFlows</button>
10661115
<button class="view-tab" id="tabScheduler" onclick="switchView('scheduler')">Scheduler</button>
1116+
<button class="view-tab" id="tabLogs" onclick="switchView('logs')">Logs</button>
10671117
<button class="view-tab" id="tabSettings" onclick="switchView('settings')">Settings</button>
10681118
</div>
10691119
</div>
@@ -1413,6 +1463,34 @@ <h3 style="margin:0;color:#8b949e;font-size:12px;text-transform:uppercase;letter
14131463
<div id="schedulesList"></div>
14141464
</div>
14151465
</div>
1466+
<div class="logs-view hidden" id="logsView">
1467+
<div class="logs-toolbar">
1468+
<div class="logs-filters">
1469+
<select id="logSourceFilter" onchange="applyLogFilters()">
1470+
<option value="">All Sources</option>
1471+
<option value="voice">Voice</option>
1472+
<option value="telegram">Telegram</option>
1473+
<option value="whatsapp">WhatsApp</option>
1474+
<option value="triggers">Triggers</option>
1475+
<option value="sdk">SDK</option>
1476+
<option value="scheduler">Scheduler</option>
1477+
<option value="system">System</option>
1478+
</select>
1479+
<select id="logLevelFilter" onchange="applyLogFilters()">
1480+
<option value="">All Levels</option>
1481+
<option value="info">Info</option>
1482+
<option value="warn">Warn</option>
1483+
<option value="error">Error</option>
1484+
</select>
1485+
<input id="logSearchInput" type="text" placeholder="Search logs..." oninput="applyLogFilters()">
1486+
</div>
1487+
<div class="logs-actions">
1488+
<label class="logs-autoscroll-label"><input type="checkbox" id="logAutoScroll" checked> Auto-scroll</label>
1489+
<button onclick="clearLogView()">Clear</button>
1490+
</div>
1491+
</div>
1492+
<div class="logs-output" id="logsOutput"></div>
1493+
</div>
14161494
<div class="settings-view hidden" id="settingsView">
14171495
<div style="max-width:560px;width:100%;margin:0 auto;">
14181496
<h3 style="margin:0 0 16px;color:#e6edf3;font-size:16px;">Settings</h3>
@@ -1468,7 +1546,7 @@ <h3 style="margin:0 0 16px;color:#e6edf3;font-size:16px;">Settings</h3>
14681546
<script>
14691547
var hasComposio={{HAS_COMPOSIO}};
14701548
if(!hasComposio){document.getElementById('tabIntegrations').style.display='none';}
1471-
let currentView='chat',filesLoaded=false,integrationsLoaded=false,commsLoaded=false,skillsLoaded=false,flowsLoaded=false,schedulerLoaded=false,settingsLoaded=false;
1549+
let currentView='chat',filesLoaded=false,integrationsLoaded=false,commsLoaded=false,skillsLoaded=false,flowsLoaded=false,schedulerLoaded=false,logsLoaded=false,settingsLoaded=false;
14721550
function switchView(v){
14731551
currentView=v;
14741552
document.getElementById('chatView').classList.toggle('hidden',v!=='chat');
@@ -1477,19 +1555,22 @@ <h3 style="margin:0 0 16px;color:#e6edf3;font-size:16px;">Settings</h3>
14771555
document.getElementById('skillsView').classList.toggle('hidden',v!=='skills');
14781556
document.getElementById('flowsView').classList.toggle('hidden',v!=='flows');
14791557
document.getElementById('schedulerView').classList.toggle('hidden',v!=='scheduler');
1558+
document.getElementById('logsView').classList.toggle('hidden',v!=='logs');
14801559
document.getElementById('settingsView').classList.toggle('hidden',v!=='settings');
14811560
document.getElementById('tabChat').classList.toggle('active',v==='chat');
14821561
document.getElementById('tabIntegrations').classList.toggle('active',v==='integrations');
14831562
document.getElementById('tabComms').classList.toggle('active',v==='comms');
14841563
document.getElementById('tabSkills').classList.toggle('active',v==='skills');
14851564
document.getElementById('tabFlows').classList.toggle('active',v==='flows');
14861565
document.getElementById('tabScheduler').classList.toggle('active',v==='scheduler');
1566+
document.getElementById('tabLogs').classList.toggle('active',v==='logs');
14871567
document.getElementById('tabSettings').classList.toggle('active',v==='settings');
14881568
if(v==='integrations'&&!integrationsLoaded){loadToolkits();integrationsLoaded=true;}
14891569
if(v==='comms'&&!commsLoaded){loadTelegramStatus();loadWhatsAppStatus();loadPhoneWebhookUrl();commsLoaded=true;}
14901570
if(v==='skills'&&!skillsLoaded){document.getElementById('skillsFrame').src='/api/skills-mp/proxy?path=/';skillsLoaded=true;}
14911571
if(v==='flows'){loadFlowSkills();loadSavedFlows();flowsLoaded=true;}
14921572
if(v==='scheduler'&&!schedulerLoaded){loadSchedules();schedulerLoaded=true;}
1573+
if(v==='logs'&&!logsLoaded){loadLogs();logsLoaded=true;}
14931574
if(v==='settings'&&!settingsLoaded){loadSettings();settingsLoaded=true;}
14941575
}
14951576
// Skills MP install handler
@@ -2038,6 +2119,9 @@ <h3 style="margin:0 0 16px;color:#e6edf3;font-size:16px;">Settings</h3>
20382119
setStatus('Connected','connected');
20392120
if(filesLoaded)loadFileTree();
20402121
refreshFileTree();break;
2122+
case'log_entry':
2123+
appendLogEntry(m.entry);
2124+
break;
20412125
case'memory_saving':
20422126
if(m.status==='start'){
20432127
var msEl=document.createElement('div');msEl.className='conv-msg memory-saving';msEl.id='memory-saving-indicator';
@@ -2788,6 +2872,83 @@ <h3 style="margin:0 0 16px;color:#e6edf3;font-size:16px;">Settings</h3>
27882872
{value:'deepseek:deepseek-chat',label:'DeepSeek Chat'},
27892873
{value:'',label:'Custom (enter below)'}
27902874
];
2875+
// ── Logs viewer ─────────────────────────────────────────────────────
2876+
var logEntries=[];
2877+
var logMaxId=0;
2878+
var LOG_CLIENT_CAP=5000;
2879+
2880+
function loadLogs(){
2881+
fetch('/api/logs').then(function(r){return r.json();}).then(function(d){
2882+
if(d.entries&&d.entries.length){
2883+
logEntries=d.entries;
2884+
logMaxId=d.entries[d.entries.length-1].id;
2885+
renderAllLogs();
2886+
}
2887+
}).catch(function(){});
2888+
}
2889+
2890+
function appendLogEntry(entry){
2891+
logEntries.push(entry);
2892+
if(entry.id>logMaxId)logMaxId=entry.id;
2893+
if(logEntries.length>LOG_CLIENT_CAP)logEntries=logEntries.slice(logEntries.length-LOG_CLIENT_CAP);
2894+
if(matchesLogFilter(entry)){
2895+
var el=renderLogEntry(entry);
2896+
var out=document.getElementById('logsOutput');
2897+
out.appendChild(el);
2898+
if(document.getElementById('logAutoScroll').checked)out.scrollTop=out.scrollHeight;
2899+
}
2900+
}
2901+
2902+
function renderLogEntry(entry){
2903+
var row=document.createElement('div');row.className='log-entry';
2904+
var ts=document.createElement('span');ts.className='log-ts';
2905+
var d=new Date(entry.ts);
2906+
ts.textContent=d.toLocaleTimeString('en-US',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'})+'.'+String(d.getMilliseconds()).padStart(3,'0');
2907+
var src=document.createElement('span');
2908+
src.className='log-src log-src-'+(entry.source||'system');
2909+
src.textContent=entry.source||'system';
2910+
var msg=document.createElement('span');
2911+
msg.className='log-msg log-'+entry.level;
2912+
msg.textContent=entry.message;
2913+
row.appendChild(ts);row.appendChild(src);row.appendChild(msg);
2914+
return row;
2915+
}
2916+
2917+
function matchesLogFilter(entry){
2918+
var sf=document.getElementById('logSourceFilter').value;
2919+
var lf=document.getElementById('logLevelFilter').value;
2920+
var q=(document.getElementById('logSearchInput').value||'').toLowerCase();
2921+
if(sf&&entry.source!==sf)return false;
2922+
if(lf&&entry.level!==lf)return false;
2923+
if(q&&entry.message.toLowerCase().indexOf(q)===-1)return false;
2924+
return true;
2925+
}
2926+
2927+
function applyLogFilters(){
2928+
var out=document.getElementById('logsOutput');out.innerHTML='';
2929+
for(var i=0;i<logEntries.length;i++){
2930+
if(matchesLogFilter(logEntries[i])){
2931+
out.appendChild(renderLogEntry(logEntries[i]));
2932+
}
2933+
}
2934+
if(document.getElementById('logAutoScroll').checked)out.scrollTop=out.scrollHeight;
2935+
}
2936+
2937+
function clearLogView(){
2938+
logEntries=[];
2939+
document.getElementById('logsOutput').innerHTML='';
2940+
}
2941+
2942+
function renderAllLogs(){
2943+
var out=document.getElementById('logsOutput');out.innerHTML='';
2944+
for(var i=0;i<logEntries.length;i++){
2945+
if(matchesLogFilter(logEntries[i])){
2946+
out.appendChild(renderLogEntry(logEntries[i]));
2947+
}
2948+
}
2949+
if(document.getElementById('logAutoScroll').checked)out.scrollTop=out.scrollHeight;
2950+
}
2951+
27912952
function loadSettings(){
27922953
fetch('/api/settings').then(function(r){return r.json();}).then(function(d){
27932954
var sel=document.getElementById('settingsModel');

0 commit comments

Comments
 (0)