Skip to content

Activity & logging

The Activity log is Verql’s single, unified stream of “what’s happening” — queries the app ran, AI/MCP tool calls, connection lifecycle, notifications, outbound network requests, and general diagnostic log lines from the glue. One stream feeds two audiences: users (a readable, filterable record of what the app did) and developers (in-app diagnostics, no log files to dig out of userData). It is also exposed to agents read-only via a shared tool.

It follows Verql’s orchestrator + plugins rule: the host owns the stream (glue); recording happens at the points where things actually occur.

LayerOwnsLives in
ActivityLog (host glue)an in-memory ring buffer (cap 1000); record / list / subscribe / clear; provided as the activity-log servicesrc/main/activity/log.ts
ActivityBatcher (host glue)coalesces appended entries into batches before they cross IPCsrc/main/activity/batcher.ts
Logger (host glue)mirrors a log line to the console and records it as a log entry; provided as the logger servicesrc/main/logging/logger.ts
Recorderscall activityLog.record(...) where things happen (db queries, connect/disconnect, tool calls, notifications, network)src/main/ipc/db.ts, ipc-handlers.ts, src/main/mcp/server.ts, …
Renderer storemirrors the stream (cap 1000), applies each IPC batch in one updatesrc/renderer/src/stores/activity.ts
Activity panelfilter (kind + level), search, pause, export, expand-detail UIsrc/renderer/src/components/shell/ActivityPanel.tsx

Entries are deliberately free of secrets, and every stored text field is clipped to 2000 chars so a giant SQL/error can’t bloat the ring or the IPC payload.

Activity entries have a kind and a level. Alongside the existing kinds (query, tool-call, connection, notification, network) there is a log kind for general diagnostics, and the level set adds debug (so debuginfosuccess / warnerror).

createLogger(sink, scope) returns a Logger with debug / info / warn / error(message, detail?) and a child(scope) for narrower scopes (appapp:plugins). Each call:

  1. mirrors to the matching console method (terminal / devtools unchanged), and
  2. records a log entry — title = message, source = scope, detail = the serialized detail (an Error becomes its stack; an object becomes pretty JSON).

The host provides it as the logger service so plugins can log into the same stream, and wires a few glue call-sites (plugin boot, MCP auto-start, drag-drop) through it instead of raw console.error.

Recording is cheap, but one IPC message per entry is not — a migration, a chatty AI loop, or verbose debug logging can produce a burst. Two seams keep the UI smooth:

  • Batching (main → renderer). ActivityLog.subscribe feeds an ActivityBatcher, which buffers entries and flushes a single activity:batch IPC payload when either the buffer hits maxBatch (50) or intervalMs (100 ms) elapses since the first buffered entry. The renderer applies each batch in one Zustand set, so a burst is a few re-renders, not hundreds.
  • Pause (renderer). The panel can freeze on a snapshot so a fast stream can’t yank rows out from under a reader; entries keep accumulating in the store and reappear on resume.

The panel also caps rendered rows (400) independently of how many the store keeps, while export still sees every matching entry.

ActivityList is presentational and store-free (used by the panel and Storybook); it owns its own filter/search/pause state. ActivityPanel is the thin container that wires the live store. Controls: search (free-text across title, detail, and source), kind and level chips (multi-select; empty = show all), pause / resume, export (download the matching entries as verql-activity-<ts>.json), and clear.

type ActivityKind = 'query' | 'tool-call' | 'connection' | 'notification' | 'network' | 'log'
type ActivityLevel = 'debug' | 'info' | 'success' | 'warn' | 'error'
interface ActivityEntry {
id: string
ts: number // epoch ms
kind: ActivityKind
level: ActivityLevel
title: string // short headline
detail?: string // longer text (SQL, error, serialized log detail)
source?: string // connection id/name, provider/tool id, or logger scope
durationMs?: number
}

ActivityQuery filters list() by kinds, levels, sinceTs, and limit.

ConcernFile
Shared typesshared/activity.ts
Activity ring buffersrc/main/activity/log.ts
IPC batchersrc/main/activity/batcher.ts
App loggersrc/main/logging/logger.ts
Host wiring (provide services, stream batches, recorders)src/main/ipc-handlers.ts
IPC channels / eventsshared/ipc.ts (activity:list, activity:clear, activity:batch)
Renderer storesrc/renderer/src/stores/activity.ts
Activity panel UIsrc/renderer/src/components/shell/ActivityPanel.tsx

See also: Notifications for the attention seam (a separate concern — “your response is needed”, not a passive record), and Architecture for where the activity log sits among the main-process subsystems.