claude-code-session-manager 0.62.0 → 0.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/AgentLibrary-BeJJa_zv.js +1 -0
- package/dist/assets/History-mWbqemhZ.js +2 -0
- package/dist/assets/Hooks-D_8sciZu.js +3 -0
- package/dist/assets/HostBilko-6aUJ7Smz.js +1 -0
- package/dist/assets/Library-CG1LDXLw.js +46 -0
- package/dist/assets/ListDetail-CIIJOWwI.js +1 -0
- package/dist/assets/MarkdownEditor-D0-y8V-f.js +1 -0
- package/dist/assets/McpServers-Kc-djiVi.js +2 -0
- package/dist/assets/Memory-B60DGRTW.js +8 -0
- package/dist/assets/Panel-CbTPsYHq.js +1 -0
- package/dist/assets/Permissions-C8KrSfrd.js +3 -0
- package/dist/assets/Plugins-BT8EumHb.js +2 -0
- package/dist/assets/ProvenanceBadge-cEqPpFsT.js +1 -0
- package/dist/assets/Scheduler-DbKPd0or.js +14 -0
- package/dist/assets/ScopeSwitcher-BNesCJ_t.js +1 -0
- package/dist/assets/Settings-Bjt7AQ6G.js +3 -0
- package/dist/assets/SkillReferenceGraph-DzuaPDM-.js +46 -0
- package/dist/assets/Skills-CVL_4X3T.js +3 -0
- package/dist/assets/SystemPrompt-Ct__nFF1.js +1 -0
- package/dist/assets/TagLibrary-DmrTfw4D.js +1 -0
- package/dist/assets/{TiptapBody-Bn-LMce4.js → TiptapBody-DaS0M_Ni.js} +1 -1
- package/dist/assets/Toggle-C0a-6xV7.js +1 -0
- package/dist/assets/index-CO_7DroC.js +3066 -0
- package/dist/assets/{index-DOXvph-Z.css → index-LlWpj2VJ.css} +1 -1
- package/dist/assets/listSkills-QORduPIk.js +1 -0
- package/dist/assets/settingsSchema-B7dMJaix.js +3 -0
- package/dist/assets/skillFrontmatter-Dif5JIg7.js +10 -0
- package/dist/index.html +2 -2
- package/package.json +1 -3
- package/src/main/__tests__/config-readText-bounded.test.cjs +84 -0
- package/src/main/__tests__/heapSnapshot.test.cjs +121 -0
- package/src/main/__tests__/historyAggregatorIntraday.test.cjs +313 -0
- package/src/main/__tests__/runLogRetention.test.cjs +343 -0
- package/src/main/__tests__/runVerify-transcript-commit-evidence.test.cjs +215 -0
- package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +42 -1
- package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +54 -1
- package/src/main/__tests__/transcripts-batch-flush.test.cjs +249 -0
- package/src/main/__tests__/transcripts-doFlush-array.test.cjs +6 -0
- package/src/main/__tests__/transcripts-paged-reads.test.cjs +6 -0
- package/src/main/__tests__/transcriptsUsageFor.test.cjs +6 -0
- package/src/main/config.cjs +32 -29
- package/src/main/files.cjs +20 -0
- package/src/main/health.cjs +32 -0
- package/src/main/heapSnapshot.cjs +122 -0
- package/src/main/historyAggregator.cjs +176 -36
- package/src/main/index.cjs +10 -63
- package/src/main/ipcSchemas.cjs +20 -209
- package/src/main/lib/opsOwnership.cjs +0 -2
- package/src/main/lib/runLogRetention.cjs +358 -0
- package/src/main/lib/summarize.cjs +0 -1
- package/src/main/runVerify.cjs +55 -6
- package/src/main/scheduler.cjs +46 -7
- package/src/main/transcripts.cjs +29 -5
- package/src/preload/api.d.ts +18 -199
- package/src/preload/index.cjs +12 -102
- package/dist/assets/index-BfiMRkln.js +0 -3225
- package/src/main/__tests__/browserAgentServer.test.cjs +0 -235
- package/src/main/__tests__/browserView-destroyed-handler.test.cjs +0 -85
- package/src/main/__tests__/browserView-oauth-popup.test.cjs +0 -126
- package/src/main/__tests__/web-remote-e2e-pinning.test.cjs +0 -181
- package/src/main/browserAgentServer.cjs +0 -224
- package/src/main/browserCapture.cjs +0 -728
- package/src/main/browserView.cjs +0 -916
- package/src/main/lib/__tests__/browserAgentActions.test.cjs +0 -76
- package/src/main/lib/browserAgentActions.cjs +0 -114
- package/src/main/lib/e2eStateMachine.cjs +0 -39
- package/src/main/webRemote.cjs +0 -1367
- package/src/preload/browserViewPreload.cjs +0 -118
|
@@ -74,6 +74,34 @@ class LRUCache {
|
|
|
74
74
|
*/
|
|
75
75
|
const aggrCache = new LRUCache(CACHE_MAX);
|
|
76
76
|
|
|
77
|
+
// ── bounded concurrency helper ───────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Run async fn(item) over items with at most `limit` in flight at once, via a
|
|
81
|
+
* pull-based worker pool (not chunked batches, so a slow item never stalls
|
|
82
|
+
* the rest of a batch). Node's fs promise APIs are backed by libuv's
|
|
83
|
+
* fixed-size threadpool (UV_THREADPOOL_SIZE, default 4) — firing every call
|
|
84
|
+
* at once via a single Promise.all still queues behind that pool one way or
|
|
85
|
+
* another, so bounding `limit` mainly avoids building thousands of pending
|
|
86
|
+
* promises/Buffers at once for a 25k+-file walk. Results are returned in
|
|
87
|
+
* input order; a thrown fn(item) rejects the whole call (callers here always
|
|
88
|
+
* catch inside fn instead).
|
|
89
|
+
*/
|
|
90
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
91
|
+
const results = new Array(items.length);
|
|
92
|
+
let next = 0;
|
|
93
|
+
async function worker() {
|
|
94
|
+
for (;;) {
|
|
95
|
+
const i = next++;
|
|
96
|
+
if (i >= items.length) return;
|
|
97
|
+
results[i] = await fn(items[i], i);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const workers = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, worker);
|
|
101
|
+
await Promise.all(workers);
|
|
102
|
+
return results;
|
|
103
|
+
}
|
|
104
|
+
|
|
77
105
|
// ── date helpers ──────────────────────────────────────────────────────────────
|
|
78
106
|
|
|
79
107
|
function decodeCwd(encoded) {
|
|
@@ -558,63 +586,171 @@ async function finalizeClosedDays({ budgetMs, dryRun = false } = {}) {
|
|
|
558
586
|
return { finalizedDates: partial ? [] : Array.from(dates), partial };
|
|
559
587
|
}
|
|
560
588
|
|
|
589
|
+
// ── intraday fast-walk state ──────────────────────────────────────────────────
|
|
590
|
+
//
|
|
591
|
+
// Two persistent (in-memory only, process-lifetime) caches make repeat ticks
|
|
592
|
+
// cheap:
|
|
593
|
+
//
|
|
594
|
+
// 1. `intradayDirRegistry` — encodedCwd -> { mtimeMs, fileNames }. Verified
|
|
595
|
+
// empirically on this machine (ext4): appending to an EXISTING file does
|
|
596
|
+
// NOT change its parent directory's mtime — only creating/removing/
|
|
597
|
+
// renaming a directory entry does. So a directory's mtime is a safe signal
|
|
598
|
+
// for "no file was added or removed here since last tick", letting us skip
|
|
599
|
+
// re-`readdir()`ing it (2,044 directories on the dev machine — this alone
|
|
600
|
+
// doesn't bound the file-level stat count).
|
|
601
|
+
//
|
|
602
|
+
// 2. `intradayNotToday` — a permanent Set<filePath> of files CONFIRMED to
|
|
603
|
+
// belong to a day other than today, so they can never contribute to
|
|
604
|
+
// today's bucket again. This is the lever that makes the walk actually
|
|
605
|
+
// proportional to files modified today, and it's safe only because of an
|
|
606
|
+
// existing, unrelated invariant this PRD does not change: a transcript's
|
|
607
|
+
// `sessionDate` is fixed by its FIRST message's timestamp and never
|
|
608
|
+
// updates on later appends (see parseJSONL's cache — `merged.sessionDate
|
|
609
|
+
// = prev.sessionDate`), so `aggregate()`/`refreshIntradayToday()` already
|
|
610
|
+
// attribute 100% of a file's activity to its original day no matter how
|
|
611
|
+
// many later days it's appended on. A file whose mtime is confirmed to
|
|
612
|
+
// predate today can have no content newer than that mtime, so its
|
|
613
|
+
// sessionDate can't be today either — that classification is exclusion
|
|
614
|
+
// forever, not just for the rest of today. Combined with #1 (a file
|
|
615
|
+
// relevant to today must have been CREATED today, which does bump its
|
|
616
|
+
// directory's mtime and is therefore always discovered), the walk over
|
|
617
|
+
// time shrinks to: files still active today (small, must keep re-stat-ing
|
|
618
|
+
// to catch growth) + any brand-new file since the last tick. Everything
|
|
619
|
+
// else is skipped without even a stat(2) call. Remaining stat/readdir
|
|
620
|
+
// calls run with bounded concurrency instead of one sequential await per
|
|
621
|
+
// call, since that was the other half of the sequential-walk cost
|
|
622
|
+
// (29,625 stats + 2,044 readdirs, 589ms measured on this machine).
|
|
623
|
+
let intradayDirRegistry = new Map(); // encodedCwd -> { mtimeMs, fileNames: string[] }
|
|
624
|
+
let intradayNotToday = new Set(); // filePath -> confirmed not-today, forever
|
|
625
|
+
|
|
626
|
+
const INTRADAY_STAT_CONCURRENCY = 64;
|
|
627
|
+
const INTRADAY_STRATEGY = 'directory-mtime skips re-readdir of unchanged dirs (new-file detection); ' +
|
|
628
|
+
'a file confirmed to belong to a past day (mtime, then sessionDate) is excluded from every future tick ' +
|
|
629
|
+
'(sessionDate is pinned to a transcript\'s first message and never changes on later appends); ' +
|
|
630
|
+
`remaining stat/readdir calls run with bounded concurrency (${INTRADAY_STAT_CONCURRENCY} in flight)`;
|
|
631
|
+
|
|
632
|
+
/** Test-only: clears the in-memory directory/exclusion registries between fixtures. */
|
|
633
|
+
function resetIntradayRegistryForTests() {
|
|
634
|
+
intradayDirRegistry = new Map();
|
|
635
|
+
intradayNotToday = new Set();
|
|
636
|
+
}
|
|
637
|
+
|
|
561
638
|
/**
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
* the
|
|
566
|
-
* on a timer (HISTORY_INTRADAY_REFRESH_MS) plus once at app start.
|
|
567
|
-
*
|
|
568
|
-
* readRollup/appendRollupDays last-write-wins per (date, projectDir, modelId)
|
|
569
|
-
* key, so re-running this simply replaces today's previous provisional lines
|
|
570
|
-
* with fresher ones — no separate "clear provisional" step needed.
|
|
639
|
+
* Pure walk-and-parse step (no rollup write) — split out from
|
|
640
|
+
* refreshIntradayToday() so it can be benchmarked read-only against the real
|
|
641
|
+
* ~/.claude/projects (see scripts/bench-intraday-walk.cjs) without touching
|
|
642
|
+
* the rollup file on disk.
|
|
571
643
|
*/
|
|
572
|
-
async function
|
|
644
|
+
async function computeIntradayBuckets() {
|
|
573
645
|
const today = localDate(new Date());
|
|
574
646
|
|
|
575
647
|
let projectDirs;
|
|
576
648
|
try {
|
|
577
649
|
projectDirs = await fsp.readdir(PROJECTS_DIR, { withFileTypes: true });
|
|
578
650
|
} catch {
|
|
579
|
-
return {
|
|
651
|
+
return { today, buckets: new Map() };
|
|
580
652
|
}
|
|
581
653
|
|
|
582
|
-
const
|
|
583
|
-
for (const projEntry of projectDirs) {
|
|
584
|
-
if (!projEntry.isDirectory()) continue;
|
|
585
|
-
const encodedCwd = projEntry.name;
|
|
586
|
-
const projectDir = path.join(PROJECTS_DIR, encodedCwd);
|
|
654
|
+
const dirEntries = projectDirs.filter((e) => e.isDirectory());
|
|
587
655
|
|
|
588
|
-
|
|
589
|
-
|
|
656
|
+
// Directory count (thousands) is far smaller than file count (tens of
|
|
657
|
+
// thousands), so stat-ing every directory concurrently is cheap and tells
|
|
658
|
+
// us which ones had an entry added/removed since we last looked.
|
|
659
|
+
const dirStats = await mapWithConcurrency(dirEntries, INTRADAY_STAT_CONCURRENCY, async (entry) => {
|
|
660
|
+
const dirPath = path.join(PROJECTS_DIR, entry.name);
|
|
661
|
+
try {
|
|
662
|
+
const stat = await fsp.stat(dirPath);
|
|
663
|
+
return { encodedCwd: entry.name, dirPath, mtimeMs: stat.mtimeMs };
|
|
664
|
+
} catch {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
});
|
|
590
668
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
669
|
+
// Resolve the jsonl file list per directory — reuse the cached list (no
|
|
670
|
+
// readdir syscall) when the directory's mtime hasn't moved since the last
|
|
671
|
+
// tick; re-enumerate only directories that are new or changed. Files
|
|
672
|
+
// already known to be irrelevant to any future "today" are dropped here so
|
|
673
|
+
// they never reach the stat step below.
|
|
674
|
+
const fileCandidates = []; // { filePath, encodedCwd }
|
|
675
|
+
await mapWithConcurrency(dirStats.filter(Boolean), INTRADAY_STAT_CONCURRENCY, async (d) => {
|
|
676
|
+
const cached = intradayDirRegistry.get(d.encodedCwd);
|
|
677
|
+
let fileNames;
|
|
678
|
+
if (cached && cached.mtimeMs === d.mtimeMs) {
|
|
679
|
+
fileNames = cached.fileNames;
|
|
680
|
+
} else {
|
|
681
|
+
let files;
|
|
682
|
+
try { files = await fsp.readdir(d.dirPath, { withFileTypes: true }); } catch { files = []; }
|
|
683
|
+
fileNames = files.filter((f) => f.isFile() && f.name.endsWith('.jsonl')).map((f) => f.name);
|
|
684
|
+
intradayDirRegistry.set(d.encodedCwd, { mtimeMs: d.mtimeMs, fileNames });
|
|
685
|
+
}
|
|
686
|
+
for (const name of fileNames) {
|
|
687
|
+
const filePath = path.join(d.dirPath, name);
|
|
688
|
+
if (intradayNotToday.has(filePath)) continue;
|
|
689
|
+
fileCandidates.push({ filePath, encodedCwd: d.encodedCwd });
|
|
690
|
+
}
|
|
691
|
+
});
|
|
594
692
|
|
|
595
|
-
|
|
596
|
-
|
|
693
|
+
// Known caveat: parseJSONL separately detects a same-path file replacement
|
|
694
|
+
// (inode change — e.g. a `claude --resume` compaction rewriting a
|
|
695
|
+
// transcript from scratch) and re-parses it in full. If that happens to a
|
|
696
|
+
// path already in intradayNotToday, this walk will never look at it again
|
|
697
|
+
// to notice its (hypothetical) new sessionDate, since exclusion here skips
|
|
698
|
+
// the stat entirely. This is safe in practice: the History dashboard's
|
|
699
|
+
// actual reads (aggregate()) don't consult this cache at all and always
|
|
700
|
+
// re-walk live for "today", so the only blast radius is a stale PROVISIONAL
|
|
701
|
+
// rollup line for the affected project until the next app restart resets
|
|
702
|
+
// this cache — never a wrong number on the dashboard itself.
|
|
703
|
+
const buckets = new Map();
|
|
704
|
+
await mapWithConcurrency(fileCandidates, INTRADAY_STAT_CONCURRENCY, async ({ filePath, encodedCwd }) => {
|
|
705
|
+
let stat;
|
|
706
|
+
try { stat = await fsp.stat(filePath); } catch { return; }
|
|
707
|
+
|
|
708
|
+
// A file whose mtime is before today can't have any content from today —
|
|
709
|
+
// and per the sessionDate-pinning invariant above, can never gain any —
|
|
710
|
+
// so it's excluded forever, with no parse ever needed.
|
|
711
|
+
const mtimeDate = localDate(new Date(stat.mtimeMs));
|
|
712
|
+
if (mtimeDate < today) {
|
|
713
|
+
intradayNotToday.add(filePath);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
597
716
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
717
|
+
const { result: parsed } = await parseJSONL(filePath, stat);
|
|
718
|
+
if (parsed.skipped) return;
|
|
719
|
+
if (parsed.sessionDate !== today) {
|
|
720
|
+
intradayNotToday.add(filePath);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
602
723
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
724
|
+
const key = `${today}|${encodedCwd}`;
|
|
725
|
+
if (!buckets.has(key)) buckets.set(key, emptyBucket(today, encodedCwd));
|
|
726
|
+
foldParsedIntoBucket(buckets.get(key), parsed);
|
|
727
|
+
});
|
|
606
728
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
729
|
+
return { today, buckets };
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Refresh TODAY's rollup buckets from a live (LRU-warm, cheap) parse of every
|
|
734
|
+
* transcript touched today, and upsert them as provisional lines (v2, no
|
|
735
|
+
* finalizedAt) so the History dashboard's "today" row stays current without
|
|
736
|
+
* the renderer ever triggering a transcript scan itself. Meant to be called
|
|
737
|
+
* on a timer (HISTORY_INTRADAY_REFRESH_MS) plus once at app start.
|
|
738
|
+
*
|
|
739
|
+
* readRollup/appendRollupDays last-write-wins per (date, projectDir, modelId)
|
|
740
|
+
* key, so re-running this simply replaces today's previous provisional lines
|
|
741
|
+
* with fresher ones — no separate "clear provisional" step needed.
|
|
742
|
+
*
|
|
743
|
+
* See INTRADAY_STRATEGY / intradayDirRegistry's comment above for why this is
|
|
744
|
+
* both faster and no less correct than the old unconditional full walk.
|
|
745
|
+
*/
|
|
746
|
+
async function refreshIntradayToday() {
|
|
747
|
+
const { today, buckets } = await computeIntradayBuckets();
|
|
612
748
|
|
|
613
749
|
const entries = [];
|
|
614
750
|
for (const b of buckets.values()) entries.push(...bucketToRollupLines(b));
|
|
615
751
|
if (entries.length) await historyRollup.appendRollupDays(entries);
|
|
616
752
|
|
|
617
|
-
return { date: today, projectsUpdated: buckets.size };
|
|
753
|
+
return { date: today, projectsUpdated: buckets.size, strategy: INTRADAY_STRATEGY };
|
|
618
754
|
}
|
|
619
755
|
|
|
620
756
|
// ── aggregate ─────────────────────────────────────────────────────────────────
|
|
@@ -821,6 +957,7 @@ module.exports = {
|
|
|
821
957
|
MODEL_PRICING,
|
|
822
958
|
finalizeClosedDays,
|
|
823
959
|
refreshIntradayToday,
|
|
960
|
+
computeIntradayBuckets,
|
|
824
961
|
// exported for tests
|
|
825
962
|
scanAggrLines,
|
|
826
963
|
parseJSONL,
|
|
@@ -828,6 +965,9 @@ module.exports = {
|
|
|
828
965
|
CACHE_MAX,
|
|
829
966
|
LRUCache,
|
|
830
967
|
computeActiveMinutes,
|
|
968
|
+
mapWithConcurrency,
|
|
969
|
+
resetIntradayRegistryForTests,
|
|
970
|
+
INTRADAY_STAT_CONCURRENCY,
|
|
831
971
|
// exported for historyDashboard.cjs (pure, no I/O at require-time; reused
|
|
832
972
|
// rather than re-implemented so date-range math stays in one place)
|
|
833
973
|
localDate,
|
package/src/main/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { app, BrowserWindow, ipcMain, dialog, Menu, session, systemPreferences, globalShortcut, shell, clipboard,
|
|
1
|
+
const { app, BrowserWindow, ipcMain, dialog, Menu, session, systemPreferences, globalShortcut, shell, clipboard, powerSaveBlocker, protocol } = require('electron');
|
|
2
2
|
const { spawn, execFile, execFileSync } = require('node:child_process');
|
|
3
3
|
const path = require('node:path');
|
|
4
4
|
const fs = require('node:fs');
|
|
@@ -9,8 +9,6 @@ const { cleanChildEnv } = require('./lib/cleanEnv.cjs');
|
|
|
9
9
|
const { terminateLosingInstance } = require('./lib/singleInstanceGuard.cjs');
|
|
10
10
|
const { acquireSchedulerOwnership, releaseSchedulerOwnership } = require('./lib/instanceLock.cjs');
|
|
11
11
|
const { manager: ptyManager, registerPtyHandlers } = require('./pty.cjs');
|
|
12
|
-
const browserView = require('./browserView.cjs');
|
|
13
|
-
const browserCapture = require('./browserCapture.cjs');
|
|
14
12
|
const configMgr = require('./config.cjs');
|
|
15
13
|
const transcripts = require('./transcripts.cjs');
|
|
16
14
|
const sessionsStore = require('./sessionsStore.cjs');
|
|
@@ -40,13 +38,6 @@ const adminHttp = createAdminHttp();
|
|
|
40
38
|
scheduler.registerAdminRoutes(adminHttp);
|
|
41
39
|
prdCreate.registerAdminRoute(adminHttp, scheduler.remote);
|
|
42
40
|
chatRunner.registerAdminRoute(adminHttp);
|
|
43
|
-
const { createBrowserAgentServer } = require('./browserAgentServer.cjs');
|
|
44
|
-
const browserAgentServer = createBrowserAgentServer({
|
|
45
|
-
listTabs: () => browserView.listViews(),
|
|
46
|
-
screenshot: ({ viewId }) => browserView.captureShotForAgent({ viewId }),
|
|
47
|
-
action: (params) => browserView.dispatchViewAction(params),
|
|
48
|
-
dom: ({ viewId }) => browserView.captureAgentDom({ viewId }),
|
|
49
|
-
});
|
|
50
41
|
const supervisor = require('./supervisor.cjs');
|
|
51
42
|
const watchers = require('./watchers.cjs');
|
|
52
43
|
const teams = require('./teams.cjs');
|
|
@@ -67,9 +58,9 @@ const { registerBilkoHostIpc } = require('./bilkoHost.cjs');
|
|
|
67
58
|
const promptSessionTranscript = require('./promptSessionTranscript.cjs');
|
|
68
59
|
const agentMemory = require('./agentMemory.cjs');
|
|
69
60
|
const git = require('./git.cjs');
|
|
61
|
+
const heapSnapshot = require('./heapSnapshot.cjs');
|
|
70
62
|
const filesIpc = require('./files.cjs');
|
|
71
63
|
const { registerDocEditHandlers, attachWindow: attachDocEditWindow } = require('./docEdit.cjs');
|
|
72
|
-
const webRemote = require('./webRemote.cjs');
|
|
73
64
|
const { listExchanges } = require('./exchanges.cjs');
|
|
74
65
|
const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
|
|
75
66
|
const { checkInsideHome, assertInsideHome } = require('./lib/insideHome.cjs');
|
|
@@ -306,8 +297,6 @@ async function rebootApp() {
|
|
|
306
297
|
ptyManager.attachWindow(mainWindow);
|
|
307
298
|
configMgr.attachWindow(mainWindow);
|
|
308
299
|
transcripts.attachWindow(mainWindow);
|
|
309
|
-
browserView.attachWindow(mainWindow);
|
|
310
|
-
browserCapture.attachWindow(mainWindow);
|
|
311
300
|
voiceHotkey.init(mainWindow).catch((e) => {
|
|
312
301
|
logs.writeLine({ scope: 'voice-hotkey', level: 'error', message: 'reinit failed', meta: { error: e?.message } });
|
|
313
302
|
});
|
|
@@ -506,25 +495,6 @@ ipcMain.handle('app:pick-directory', async () => {
|
|
|
506
495
|
|
|
507
496
|
ipcMain.on('app:reboot-app', () => rebootApp());
|
|
508
497
|
|
|
509
|
-
// Recorder export "Save to file…" (PRD 412) — native Save As dialog, written
|
|
510
|
-
// directly since the path is user-chosen, not renderer input.
|
|
511
|
-
ipcMain.handle('browser:save-recording', validated(schemas.browserSaveRecording, async ({ defaultName, text }) => {
|
|
512
|
-
try {
|
|
513
|
-
const result = await dialog.showSaveDialog(mainWindow, {
|
|
514
|
-
defaultPath: defaultName,
|
|
515
|
-
filters: [
|
|
516
|
-
{ name: 'Markdown', extensions: ['md'] },
|
|
517
|
-
{ name: 'All Files', extensions: ['*'] },
|
|
518
|
-
],
|
|
519
|
-
});
|
|
520
|
-
if (result.canceled || !result.filePath) return { ok: false, canceled: true };
|
|
521
|
-
await fsp.writeFile(result.filePath, text, 'utf8');
|
|
522
|
-
return { ok: true, path: result.filePath };
|
|
523
|
-
} catch (e) {
|
|
524
|
-
return { ok: false, error: e && e.message ? e.message : String(e) };
|
|
525
|
-
}
|
|
526
|
-
}));
|
|
527
|
-
|
|
528
498
|
// Image paste — Ctrl+V in the Terminal pane. Reads the OS clipboard via
|
|
529
499
|
// Electron's native API (renderer's navigator.clipboard.read() doesn't expose
|
|
530
500
|
// raw image MIME types under contextIsolation), saves the bitmap to a temp
|
|
@@ -570,19 +540,6 @@ ipcMain.handle('clipboard:write-text', validated(schemas.clipboardWriteText, ({
|
|
|
570
540
|
}
|
|
571
541
|
}));
|
|
572
542
|
|
|
573
|
-
// PRD 407 Capture panel — write side of paste-image's read. Writes a
|
|
574
|
-
// screenshot capture to the OS clipboard as an image.
|
|
575
|
-
ipcMain.handle('browser:copy-image', validated(schemas.browserCopyImage, ({ dataUrl }) => {
|
|
576
|
-
try {
|
|
577
|
-
const img = nativeImage.createFromDataURL(dataUrl);
|
|
578
|
-
if (!img || img.isEmpty()) return { ok: false, error: 'empty image' };
|
|
579
|
-
clipboard.writeImage(img);
|
|
580
|
-
return { ok: true };
|
|
581
|
-
} catch (e) {
|
|
582
|
-
return { ok: false, error: e && e.message ? e.message : String(e) };
|
|
583
|
-
}
|
|
584
|
-
}));
|
|
585
|
-
|
|
586
543
|
// Hooks tab "Test fire": run a hook command with a fake event payload piped
|
|
587
544
|
// to stdin. shell:true is intentional — Claude Code's hook field is a shell
|
|
588
545
|
// string. Timeout is enforced via SIGKILL on a timer because spawn's built-in
|
|
@@ -841,7 +798,6 @@ agentMemory.registerAgentMemoryHandlers();
|
|
|
841
798
|
git.register(ipcMain);
|
|
842
799
|
filesIpc.registerFilesHandlers();
|
|
843
800
|
registerDocEditHandlers();
|
|
844
|
-
webRemote.registerRemoteHandlers();
|
|
845
801
|
chatRunner.registerChatHandlers();
|
|
846
802
|
|
|
847
803
|
// Direct in-process PRD authoring for a queued PromptTicket classified
|
|
@@ -875,6 +831,10 @@ ipcMain.handle('otel:set-config', async (_e, cfg) => {
|
|
|
875
831
|
ipcMain.handle('otel:status', () => otel.status());
|
|
876
832
|
ipcMain.handle('otel:config-path', () => otelSettings.storePath());
|
|
877
833
|
|
|
834
|
+
// Diagnostic-only renderer heap snapshot. No-op unless SM_HEAP_SNAPSHOT=1 —
|
|
835
|
+
// see heapSnapshot.cjs for why this stays off by default.
|
|
836
|
+
heapSnapshot.registerIpc({ ipcMain, getWindow: () => mainWindow });
|
|
837
|
+
|
|
878
838
|
// --- App lifecycle ---
|
|
879
839
|
|
|
880
840
|
// E2E under xvfb-run has no working GPU; the GPU process exits ~15 seconds
|
|
@@ -943,11 +903,6 @@ app.on('web-contents-created', (_e, wc) => {
|
|
|
943
903
|
});
|
|
944
904
|
|
|
945
905
|
wc.on('will-navigate', (event, url) => {
|
|
946
|
-
// The embedded Browser tab's WebContentsView is a real browser — it must
|
|
947
|
-
// be able to navigate anywhere. Exempt ONLY webContents registered by
|
|
948
|
-
// browserView.cjs; the main window's lock below is unchanged.
|
|
949
|
-
if (browserView.isBrowserViewContents(wc.id)) return;
|
|
950
|
-
|
|
951
906
|
const allowed = useDevServer
|
|
952
907
|
? ['http://localhost:5173', 'http://127.0.0.1:5173']
|
|
953
908
|
: [];
|
|
@@ -1087,6 +1042,8 @@ app.whenReady().then(async () => {
|
|
|
1087
1042
|
try { await systemPreferences.askForMediaAccess('microphone'); } catch { /* */ }
|
|
1088
1043
|
}
|
|
1089
1044
|
|
|
1045
|
+
const heapSnapshotMenuItem = heapSnapshot.buildMenuItem(() => mainWindow);
|
|
1046
|
+
|
|
1090
1047
|
const template = [
|
|
1091
1048
|
{
|
|
1092
1049
|
label: 'Session Manager',
|
|
@@ -1123,6 +1080,8 @@ app.whenReady().then(async () => {
|
|
|
1123
1080
|
submenu: [
|
|
1124
1081
|
{ label: 'Toggle DevTools', accelerator: 'F12', role: 'toggleDevTools' },
|
|
1125
1082
|
{ label: 'Reload', accelerator: 'CmdOrCtrl+R', role: 'reload' },
|
|
1083
|
+
// Diagnostic-only; absent unless SM_HEAP_SNAPSHOT=1 (see heapSnapshot.cjs).
|
|
1084
|
+
...(heapSnapshotMenuItem ? [heapSnapshotMenuItem] : []),
|
|
1126
1085
|
],
|
|
1127
1086
|
},
|
|
1128
1087
|
{
|
|
@@ -1144,16 +1103,12 @@ app.whenReady().then(async () => {
|
|
|
1144
1103
|
ptyManager.attachWindow(mainWindow);
|
|
1145
1104
|
configMgr.attachWindow(mainWindow);
|
|
1146
1105
|
transcripts.attachWindow(mainWindow);
|
|
1147
|
-
browserView.registerBrowserView({ mainWindow, ipcMain });
|
|
1148
|
-
browserCapture.attachWindow(mainWindow);
|
|
1149
|
-
browserCapture.registerBrowserCapture({ ipcMain, getView: browserView.getView });
|
|
1150
1106
|
voiceHotkey.init(mainWindow).catch((e) => {
|
|
1151
1107
|
logs.writeLine({ scope: 'voice-hotkey', level: 'error', message: 'init failed', meta: { error: e?.message } });
|
|
1152
1108
|
});
|
|
1153
1109
|
scheduler.attachWindow(mainWindow);
|
|
1154
1110
|
watchers.attachWindow(mainWindow);
|
|
1155
1111
|
pluginInstall.attachWindow(mainWindow);
|
|
1156
|
-
webRemote.attachWindow(mainWindow);
|
|
1157
1112
|
chatRunner.attachWindow(mainWindow);
|
|
1158
1113
|
promptSessionEvents.attachWindow(mainWindow);
|
|
1159
1114
|
attachDocEditWindow(mainWindow);
|
|
@@ -1177,18 +1132,12 @@ app.whenReady().then(async () => {
|
|
|
1177
1132
|
console.log(`[main] scheduler ownership held by pid ${ownership.holderPid} — running scheduler-passive (no reconciliation, no admin server)`);
|
|
1178
1133
|
logs.writeLine({ scope: 'scheduler', level: 'info', message: 'scheduler-passive: ownership held by another instance', meta: { holderPid: ownership.holderPid } });
|
|
1179
1134
|
}
|
|
1180
|
-
browserAgentServer.start().catch((e) => {
|
|
1181
|
-
logs.writeLine({ scope: 'browser-agent-server', level: 'error', message: 'init failed', meta: { error: e?.message } });
|
|
1182
|
-
});
|
|
1183
1135
|
// First-boot default: install the bundled session-manager-dev plugin (its 10
|
|
1184
1136
|
// dev skills) from the app's own marketplace. One-shot + idempotent; never
|
|
1185
1137
|
// throws. SM_SEED_DEV_PLUGIN_DISABLE=1 to opt out.
|
|
1186
1138
|
seedDevPlugin({ logger: console }).catch((e) => {
|
|
1187
1139
|
logs.writeLine({ scope: 'seed-dev-plugin', level: 'error', message: 'seed failed', meta: { error: e?.message } });
|
|
1188
1140
|
});
|
|
1189
|
-
webRemote.init().catch((e) => {
|
|
1190
|
-
logs.writeLine({ scope: 'webRemote', level: 'error', message: 'init failed', meta: { error: e?.message } });
|
|
1191
|
-
});
|
|
1192
1141
|
// History rollup finalize pass: deferred 30s past boot so it never competes
|
|
1193
1142
|
// with first-paint, fire-and-forget (cron/offline refresh is PRD 651 — this
|
|
1194
1143
|
// is just the one-shot in-app pass).
|
|
@@ -1249,7 +1198,6 @@ let teardownDone = false;
|
|
|
1249
1198
|
function runShutdownCleanup() {
|
|
1250
1199
|
if (teardownDone) return; // idempotent — will-quit may still fire after an app.exit path
|
|
1251
1200
|
teardownDone = true;
|
|
1252
|
-
webRemote.destroy();
|
|
1253
1201
|
// Mark a clean exit so the next boot can distinguish a graceful quit from an
|
|
1254
1202
|
// OOM-kill / native crash (which leaves the sentinel `open`).
|
|
1255
1203
|
crashDiagnostics.markCleanShutdown();
|
|
@@ -1287,7 +1235,6 @@ app.on('before-quit', () => {
|
|
|
1287
1235
|
transcripts.closeAll();
|
|
1288
1236
|
watchers.manager.killAll();
|
|
1289
1237
|
adminHttp.stop().catch(() => {});
|
|
1290
|
-
browserAgentServer.stop().catch(() => {});
|
|
1291
1238
|
// Best-effort flush of any pending OTEL spans. shutdown() has its own 2s
|
|
1292
1239
|
// ceiling so a wedged exporter can't hold quit.
|
|
1293
1240
|
otel.shutdown().catch(() => {});
|