feat(app): embed indexer in Electron with file-watching service and UI refinements

Extract schema DDL into scripts/schema.sql shared between CLI and app.
  Add an in-process chokidar-based indexer-service that watches ~/.claude/projects
  for JSONL changes, debounces, and triggers background rebuilds via a worker
  thread. Rename Usage view to Activity, flesh out MemoryDetail and SubagentDetail
  views, and refine App.vue layout/routing. The main process now starts/stops the
  indexer lifecycle and notifies renderer windows on index updates.
This commit is contained in:
tommy0103
2026-06-13 03:42:01 +08:00
parent b524339d85
commit 4eec6b38c9
29 changed files with 1370 additions and 314 deletions
+1 -60
View File
@@ -8,66 +8,7 @@ const { DatabaseSync } = require('node:sqlite');
const CLAUDE_DIR = path.join(os.homedir(), '.claude');
const DB_PATH = path.join(CLAUDE_DIR, 'obelisk.sqlite');
const TEXT_LIMIT = 10000;
const SCHEMA = `
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY, title TEXT, project TEXT, project_path TEXT,
started_at TEXT, ended_at TEXT, git_branch TEXT, version TEXT,
message_count INTEGER DEFAULT 0, jsonl_path TEXT);
CREATE TABLE IF NOT EXISTS messages (
uuid TEXT PRIMARY KEY, session_id TEXT, type TEXT, parent_uuid TEXT,
timestamp TEXT, role TEXT, text TEXT, content_type TEXT,
is_meta INTEGER DEFAULT 0, model TEXT,
is_sidechain INTEGER DEFAULT 0, agent_id TEXT,
input_tokens INTEGER, output_tokens INTEGER,
cwd TEXT, skill TEXT, turn_duration_ms INTEGER);
CREATE TABLE IF NOT EXISTS tool_calls (
id TEXT PRIMARY KEY, message_uuid TEXT, session_id TEXT,
name TEXT, input_json TEXT, file_path TEXT);
CREATE TABLE IF NOT EXISTS tool_results (
tool_use_id TEXT PRIMARY KEY, message_uuid TEXT, session_id TEXT,
content TEXT, file_path TEXT, is_error INTEGER DEFAULT 0);
CREATE TABLE IF NOT EXISTS subagents (
agent_id TEXT PRIMARY KEY, session_id TEXT, parent_tool_use_id TEXT,
agent_type TEXT, description TEXT, duration_ms INTEGER, total_tokens INTEGER);
CREATE TABLE IF NOT EXISTS workflows (
run_id TEXT PRIMARY KEY, session_id TEXT, task_id TEXT,
script TEXT, result_json TEXT, timestamp TEXT, agent_count INTEGER DEFAULT 0,
duration_ms INTEGER, total_tokens INTEGER, status TEXT, workflow_name TEXT);
CREATE TABLE IF NOT EXISTS workflow_agents (
agent_id TEXT PRIMARY KEY, run_id TEXT, session_id TEXT,
agent_type TEXT, description TEXT,
phase TEXT, label TEXT, model TEXT, state TEXT,
duration_ms INTEGER, tokens INTEGER, tool_calls INTEGER);
CREATE TABLE IF NOT EXISTS index_state (
jsonl_path TEXT PRIMARY KEY, mtime REAL, lines_processed INTEGER);
CREATE TABLE IF NOT EXISTS summaries (
id TEXT PRIMARY KEY, session_id TEXT, timestamp TEXT,
source TEXT, content TEXT);
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
uuid UNINDEXED, session_id UNINDEXED, text, content=messages, content_rowid=rowid);
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
CREATE INDEX IF NOT EXISTS idx_messages_agent ON messages(agent_id);
CREATE INDEX IF NOT EXISTS idx_messages_ts ON messages(session_id, timestamp);
CREATE INDEX IF NOT EXISTS idx_tc_session_name ON tool_calls(session_id, name);
CREATE INDEX IF NOT EXISTS idx_tc_file ON tool_calls(file_path);
CREATE INDEX IF NOT EXISTS idx_sa_session ON subagents(session_id);
CREATE INDEX IF NOT EXISTS idx_wf_session ON workflows(session_id);
CREATE INDEX IF NOT EXISTS idx_wa_run ON workflow_agents(run_id);
CREATE INDEX IF NOT EXISTS idx_summaries_session ON summaries(session_id);
CREATE TABLE IF NOT EXISTS memories (
id TEXT PRIMARY KEY, session_id TEXT, project TEXT,
message_start TEXT, message_end TEXT,
path TEXT, anchors TEXT, summary TEXT, created_at TEXT,
deleted_at TEXT, deleted_reason TEXT);
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
id UNINDEXED, path, summary,
content=memories, content_rowid=rowid,
tokenize='unicode61 remove_diacritics 1');
CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project);
CREATE INDEX IF NOT EXISTS idx_memories_session ON memories(session_id);
CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(created_at);
`;
const SCHEMA = fs.readFileSync(new URL('./schema.sql', import.meta.url), 'utf8');
function openDb() {
const db = new DatabaseSync(DB_PATH);
+20 -3
View File
@@ -246,12 +246,29 @@ function indexHistory(db) {
}
const BUILD_DEBOUNCE_MS = 30000;
const APP_HEARTBEAT_FRESH_MS = 60000;
function shouldSkipBuild(db, { now = Date.now() } = {}) {
const appHeartbeat = db.prepare("SELECT mtime FROM index_state WHERE jsonl_path='__app_heartbeat__'").get();
const appSuccessfulBuild = db.prepare("SELECT mtime FROM index_state WHERE jsonl_path='__app_last_successful_build__'").get();
if (
appHeartbeat && now - appHeartbeat.mtime < APP_HEARTBEAT_FRESH_MS &&
appSuccessfulBuild && now - appSuccessfulBuild.mtime < APP_HEARTBEAT_FRESH_MS
) {
return { skip: true, reason: 'app_successful_build' };
}
const last = db.prepare("SELECT mtime FROM index_state WHERE jsonl_path='__last_build__'").get();
if (last && now - last.mtime < BUILD_DEBOUNCE_MS) {
return { skip: true, reason: 'recent_build' };
}
return { skip: false };
}
function buildIndex({ force = false } = {}) {
const db = openDb();
if (!force) {
const last = db.prepare("SELECT mtime FROM index_state WHERE jsonl_path='__last_build__'").get();
if (last && Date.now() - last.mtime < BUILD_DEBOUNCE_MS) { db.close(); return; }
const skip = shouldSkipBuild(db);
if (skip.skip) { db.close(); return; }
}
if (force) {
@@ -286,4 +303,4 @@ function buildIndex({ force = false } = {}) {
db.close();
}
export { buildIndex, inferProjectPath, refreshSessionProjectPaths };
export { buildIndex, inferProjectPath, refreshSessionProjectPaths, shouldSkipBuild };
+57
View File
@@ -0,0 +1,57 @@
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY, title TEXT, project TEXT, project_path TEXT,
started_at TEXT, ended_at TEXT, git_branch TEXT, version TEXT,
message_count INTEGER DEFAULT 0, jsonl_path TEXT);
CREATE TABLE IF NOT EXISTS messages (
uuid TEXT PRIMARY KEY, session_id TEXT, type TEXT, parent_uuid TEXT,
timestamp TEXT, role TEXT, text TEXT, content_type TEXT,
is_meta INTEGER DEFAULT 0, model TEXT,
is_sidechain INTEGER DEFAULT 0, agent_id TEXT,
input_tokens INTEGER, output_tokens INTEGER,
cwd TEXT, skill TEXT, turn_duration_ms INTEGER);
CREATE TABLE IF NOT EXISTS tool_calls (
id TEXT PRIMARY KEY, message_uuid TEXT, session_id TEXT,
name TEXT, input_json TEXT, file_path TEXT);
CREATE TABLE IF NOT EXISTS tool_results (
tool_use_id TEXT PRIMARY KEY, message_uuid TEXT, session_id TEXT,
content TEXT, file_path TEXT, is_error INTEGER DEFAULT 0);
CREATE TABLE IF NOT EXISTS subagents (
agent_id TEXT PRIMARY KEY, session_id TEXT, parent_tool_use_id TEXT,
agent_type TEXT, description TEXT, duration_ms INTEGER, total_tokens INTEGER);
CREATE TABLE IF NOT EXISTS workflows (
run_id TEXT PRIMARY KEY, session_id TEXT, task_id TEXT,
script TEXT, result_json TEXT, timestamp TEXT, agent_count INTEGER DEFAULT 0,
duration_ms INTEGER, total_tokens INTEGER, status TEXT, workflow_name TEXT);
CREATE TABLE IF NOT EXISTS workflow_agents (
agent_id TEXT PRIMARY KEY, run_id TEXT, session_id TEXT,
agent_type TEXT, description TEXT,
phase TEXT, label TEXT, model TEXT, state TEXT,
duration_ms INTEGER, tokens INTEGER, tool_calls INTEGER);
CREATE TABLE IF NOT EXISTS index_state (
jsonl_path TEXT PRIMARY KEY, mtime REAL, lines_processed INTEGER);
CREATE TABLE IF NOT EXISTS summaries (
id TEXT PRIMARY KEY, session_id TEXT, timestamp TEXT,
source TEXT, content TEXT);
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
uuid UNINDEXED, session_id UNINDEXED, text, content=messages, content_rowid=rowid);
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
CREATE INDEX IF NOT EXISTS idx_messages_agent ON messages(agent_id);
CREATE INDEX IF NOT EXISTS idx_messages_ts ON messages(session_id, timestamp);
CREATE INDEX IF NOT EXISTS idx_tc_session_name ON tool_calls(session_id, name);
CREATE INDEX IF NOT EXISTS idx_tc_file ON tool_calls(file_path);
CREATE INDEX IF NOT EXISTS idx_sa_session ON subagents(session_id);
CREATE INDEX IF NOT EXISTS idx_wf_session ON workflows(session_id);
CREATE INDEX IF NOT EXISTS idx_wa_run ON workflow_agents(run_id);
CREATE INDEX IF NOT EXISTS idx_summaries_session ON summaries(session_id);
CREATE TABLE IF NOT EXISTS memories (
id TEXT PRIMARY KEY, session_id TEXT, project TEXT,
message_start TEXT, message_end TEXT,
path TEXT, anchors TEXT, summary TEXT, created_at TEXT,
deleted_at TEXT, deleted_reason TEXT);
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
id UNINDEXED, path, summary,
content=memories, content_rowid=rowid,
tokenize='unicode61 remove_diacritics 1');
CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project);
CREATE INDEX IF NOT EXISTS idx_memories_session ON memories(session_id);
CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(created_at);