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
+36 -4
View File
@@ -2,7 +2,7 @@
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import { state } from '../store.js';
import { highlightPlain, escapeHTML, formatProjectLabel, fmtListTime } from '../utils.js';
import { highlightPlain, escapeHTML, formatProjectLabel, fmtListTime, fmtRelative } from '../utils.js';
defineOptions({ name: 'SessionList' });
@@ -22,8 +22,8 @@ const visibleSessions = computed(() => {
})
.filter(Boolean)
.sort((a, b) => {
const ta = new Date(a.started_at || 0).getTime();
const tb = new Date(b.started_at || 0).getTime();
const ta = new Date(a.ended_at || a.started_at || 0).getTime();
const tb = new Date(b.ended_at || b.started_at || 0).getTime();
return state.sortDesc ? tb - ta : ta - tb;
});
});
@@ -39,13 +39,44 @@ function projectLabel(session) {
}
function timeLabel(session) {
const ts = new Date(session.started_at || 0).getTime();
const ts = new Date(session.ended_at || session.started_at || 0).getTime();
return fmtListTime(ts);
}
function lastActiveLabel(session) {
const ts = new Date(session.ended_at || session.started_at || 0).getTime();
return fmtListTime(ts);
}
function createdLabel(session) {
const ts = new Date(session.started_at || 0).getTime();
return fmtRelative(ts);
}
function openSession(session) {
router.push({ name: 'SessionDetail', params: { id: session.id } });
}
function obeliskStyle(session) {
const created = new Date(session.started_at || 0).getTime();
const days = Math.max(0, (Date.now() - created) / 86400000);
const height = Math.min(1, Math.log(1 + days) / Math.log(1 + 365));
let color;
if (days < 7) color = '#a855f7';
else if (days < 30) color = '#6366f1';
else if (days < 90) color = '#64748b';
else color = '#475569';
const glow = days < 7 ? `0 0 4px ${color}` : 'none';
const maxHeight = 36; // px, roughly the row height minus padding
return {
height: `${Math.max(4, Math.round(height * maxHeight))}px`,
background: color,
boxShadow: glow,
};
}
</script>
<template>
@@ -63,6 +94,7 @@ function openSession(session) {
:data-session-id="s.id"
@click="openSession(s)"
>
<div class="srow-obelisk" :style="obeliskStyle(s)"></div>
<div class="srow-body">
<div class="srow-title" v-html="titleHTML(s)"></div>
<div class="srow-meta">