feat: right-click menus everywhere, workspace/session cost, long-term DB safety
Right-click menus: one portal shell (components/ContextMenu.tsx) for every item with actions - sidebar/Recents/archived session rows (SessionMenu), console tabs, pane headers, the Changes panel's files/tree/commits, the workspace home, sessions outside a workspace (SessionRowMenu: Sessions table, Dashboard, Activity, Workflow runs), prompt images, every Settings list and every Claude Code/Codex config list (FileItemMenu). Buttons stay; a delete keeps its button's confirm; the shell stops portal clicks from reaching the card that opened it. Status bar: Workspace $ and Session $ on the Workspace page, from ?lane= on GET /api/pricing/cost and the focused/replayed session's cost. Database, no data dropped: synchronous=NORMAL, journal_size_limit 64 MB, hourly PASSIVE checkpoint + PRAGMA optimize, idx_events_session_created (replacing the redundant idx_events_session), watchdog reads sessions.transcript_path, transcript parsed before the hook's write transaction, GET /api/sessions/:id?events=0 for the web page (reloads only on its own session, debounced), streamed export, transactional clear-data and chunked cleanup, subagent import reads tool_use_ids once, runtime logs rotated past 5 MB.
This commit is contained in:
+25
-9
@@ -1367,6 +1367,14 @@ router.post("/event", (req, res) => {
|
||||
return res.json({ ok: true, duplicate: true });
|
||||
}
|
||||
|
||||
// Parse the transcript BEFORE the write transaction: `processEvent` takes the
|
||||
// write lock at its first statement and reads the transcript inside it, so a
|
||||
// cold multi-MB parse held the lock and the other dashboard on this DB hit
|
||||
// SQLITE_BUSY. Warmed here, the read inside is a stat-check cache hit.
|
||||
if (tool === "claude" && typeof data.transcript_path === "string" && data.transcript_path) {
|
||||
transcriptCache.extract(data.transcript_path);
|
||||
}
|
||||
|
||||
const result = processEvent(hook_type, data, tool);
|
||||
if (!result) {
|
||||
return res.status(400).json({
|
||||
@@ -1482,6 +1490,12 @@ router.post("/event", (req, res) => {
|
||||
// sessions every 15s to detect errors that hooks missed.
|
||||
const WATCHDOG_INTERVAL_MS = 15_000;
|
||||
const STALE_THRESHOLD_MS = 10_000; // only check sessions idle for >10s
|
||||
// The watchdog's fallback for a session with no transcript_path column value.
|
||||
const lastTranscriptEvent = db.prepare(
|
||||
`SELECT data FROM events WHERE session_id = ?
|
||||
AND event_type IN ('SessionStart','UserPromptSubmit','PreToolUse','Stop','Notification')
|
||||
ORDER BY created_at DESC LIMIT 1`
|
||||
);
|
||||
|
||||
// Idle-working timeout for the no-marker interrupt case (Esc pressed before the
|
||||
// model emits anything → Claude Code writes no interrupt entry and fires no
|
||||
@@ -1541,11 +1555,11 @@ function watchdogCheck() {
|
||||
// lifecycle is owned by server/lib/codex-usage.js.
|
||||
const staleSessions = db
|
||||
.prepare(
|
||||
`SELECT s.id, s.status, s.cwd,
|
||||
(SELECT MAX(e.created_at) FROM events e WHERE e.session_id = s.id) as last_event,
|
||||
(SELECT e.data FROM events e WHERE e.session_id = s.id
|
||||
AND e.event_type IN ('SessionStart','UserPromptSubmit','PreToolUse','Stop','Notification')
|
||||
ORDER BY e.created_at DESC LIMIT 1) as last_data
|
||||
// transcript_path is the session's own column (every hook keeps it);
|
||||
// reading it out of the newest event's `data` walked a multi-KB payload
|
||||
// per session, every 15 s, in every dashboard on this database.
|
||||
`SELECT s.id, s.status, s.cwd, s.transcript_path,
|
||||
(SELECT MAX(e.created_at) FROM events e WHERE e.session_id = s.id) as last_event
|
||||
FROM sessions s
|
||||
WHERE s.status IN ('active', 'error') AND s.updated_at < ?
|
||||
AND (s.source = 'local' OR s.source IS NULL) AND s.tool = 'claude'`
|
||||
@@ -1553,11 +1567,13 @@ function watchdogCheck() {
|
||||
.all(cutoff);
|
||||
|
||||
for (const sess of staleSessions) {
|
||||
// Try to get transcript_path from event data first
|
||||
let tPath = null;
|
||||
if (sess.last_data) {
|
||||
// The session's recorded transcript first, else the newest hook payload
|
||||
// that named one (a row from before the column was filled).
|
||||
let tPath = sess.transcript_path || null;
|
||||
if (!tPath) {
|
||||
const lastData = lastTranscriptEvent.get(sess.id)?.data;
|
||||
try {
|
||||
tPath = JSON.parse(sess.last_data).transcript_path;
|
||||
tPath = lastData ? JSON.parse(lastData).transcript_path : null;
|
||||
} catch {}
|
||||
}
|
||||
// Fall back: derive from Claude's standard path layout
|
||||
|
||||
Reference in New Issue
Block a user