claude-code-session-manager 0.35.2 → 0.35.4
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/{TiptapBody-CIEmzy3k.js → TiptapBody-CVFG9ufh.js} +1 -1
- package/dist/assets/index-DASEOqMP.js +3558 -0
- package/dist/assets/index-DZgKJkf3.css +32 -0
- package/dist/index.html +2 -2
- package/package.json +3 -2
- package/plugins/session-manager-dev/.claude-plugin/plugin.json +3 -1
- package/plugins/session-manager-dev/skills/develop/SKILL.md +12 -0
- package/plugins/session-manager-dev/skills/develop/standards.md +1 -0
- package/plugins/session-manager-dev/skills/explain-to-me/SKILL.md +48 -35
- package/plugins/session-manager-dev/skills/find-opportunity/SKILL.md +83 -0
- package/plugins/session-manager-dev/skills/memory-sanitation/SKILL.md +127 -0
- package/plugins/session-manager-dev/skills/my-feedback/SKILL.md +20 -12
- package/plugins/session-manager-dev/skills/optimize-kpi/SKILL.md +4 -3
- package/plugins/session-manager-dev/skills/process-feedback/SKILL.md +33 -12
- package/src/main/__tests__/adminServer.test.cjs +175 -0
- package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +137 -0
- package/src/main/__tests__/mcpStatus.test.cjs +61 -0
- package/src/main/__tests__/runVerify.test.cjs +74 -4
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +50 -2
- package/src/main/__tests__/scheduler-autopromote.test.cjs +19 -1
- package/src/main/adminServer.cjs +157 -0
- package/src/main/browserCapture.cjs +714 -0
- package/src/main/browserView.cjs +613 -0
- package/src/main/chatRunner.cjs +60 -0
- package/src/main/config.cjs +37 -1
- package/src/main/historyAggregator.cjs +92 -6
- package/src/main/index.cjs +37 -3
- package/src/main/ipcSchemas.cjs +106 -0
- package/src/main/lib/schedulerConfig.cjs +6 -2
- package/src/main/mcpStatus.cjs +93 -0
- package/src/main/runVerify.cjs +14 -1
- package/src/main/scheduler.cjs +159 -31
- package/src/main/sessionsStore.cjs +4 -3
- package/src/preload/api.d.ts +154 -5
- package/src/preload/browserViewPreload.cjs +67 -0
- package/src/preload/index.cjs +63 -5
- package/dist/assets/index-2Om-ouz6.js +0 -3534
- package/dist/assets/index-DeIJ8SM5.css +0 -32
- package/src/main/docEditor.cjs +0 -92
package/src/main/runVerify.cjs
CHANGED
|
@@ -616,6 +616,18 @@ async function verifyRun({ runDir, prdPath, queueEntry, allJobs = [], committedD
|
|
|
616
616
|
? { ...(annotations.length ? { annotations } : {}), ...sentinelFields }
|
|
617
617
|
: undefined;
|
|
618
618
|
|
|
619
|
+
// No pattern hits is not automatically "clean": a run that neither
|
|
620
|
+
// committed anything nor ever emitted the finish-protocol sentinel likely
|
|
621
|
+
// ended before doing real work (e.g. stopped on a clarifying question).
|
|
622
|
+
// Weaker evidence than a caught transcript error, but still not clean.
|
|
623
|
+
if (sentinel === null && !committedDuringRun) {
|
|
624
|
+
issues.push({
|
|
625
|
+
verdict: 'no_verdict_sentinel',
|
|
626
|
+
reason: 'run made no commit and emitted no SCHEDULER_VERDICT sentinel — likely ended before the finish protocol (e.g. stopped on a clarifying question)',
|
|
627
|
+
priority: 1,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
619
631
|
if (issues.length === 0) {
|
|
620
632
|
const reason = annotations.length
|
|
621
633
|
? `no blocking issues (${annotations.length} annotation(s): ${annotations.map((a) => a.reason).join('; ')})`
|
|
@@ -623,7 +635,8 @@ async function verifyRun({ runDir, prdPath, queueEntry, allJobs = [], committedD
|
|
|
623
635
|
return conclude('clean', reason, null, extras);
|
|
624
636
|
}
|
|
625
637
|
|
|
626
|
-
// Pick highest-priority issue (transcript_errors > verify_unavailable
|
|
638
|
+
// Pick highest-priority issue (transcript_errors > verify_unavailable ==
|
|
639
|
+
// no_verdict_sentinel; ties keep the loop's original order via stable sort).
|
|
627
640
|
issues.sort((a, b) => b.priority - a.priority);
|
|
628
641
|
const top = issues[0];
|
|
629
642
|
|
package/src/main/scheduler.cjs
CHANGED
|
@@ -1151,6 +1151,19 @@ function isPromotableOriginal(status) {
|
|
|
1151
1151
|
return status === 'failed' || status === 'needs_review';
|
|
1152
1152
|
}
|
|
1153
1153
|
|
|
1154
|
+
/**
|
|
1155
|
+
* Pure helper: given a completed fix-plan slug (e.g. '451-fix-foo'), find the
|
|
1156
|
+
* SPECIFIC promotable original job it heals. Matches on the full
|
|
1157
|
+
* numeric-prefixed slug ('451-foo'), not just the base ('foo') — two
|
|
1158
|
+
* needs_review jobs can share a base slug with different NN prefixes
|
|
1159
|
+
* (e.g. '451-foo' and '453-foo'), and a fix plan must only heal the
|
|
1160
|
+
* lineage whose NN it was authored against. Exported for tests.
|
|
1161
|
+
*/
|
|
1162
|
+
function healTargetForFix(fixSlug, jobs) {
|
|
1163
|
+
const originalSlug = fixSlug.replace(/^(\d+)-fix-/, '$1-');
|
|
1164
|
+
return jobs.find((x) => x.slug === originalSlug && isPromotableOriginal(x.status)) || null;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1154
1167
|
/**
|
|
1155
1168
|
* Spawn an Opus investigation session for a failed job. The investigator's job
|
|
1156
1169
|
* is to read the failure log + original PRD, identify the root cause, and write
|
|
@@ -1329,6 +1342,13 @@ DO NOT attempt the fix. ONLY write the file. When the file exists, exit immediat
|
|
|
1329
1342
|
console.log(`[scheduler] investigation produced fix plan: ${fixSlug}`);
|
|
1330
1343
|
} else {
|
|
1331
1344
|
console.log(`[scheduler] investigation finished WITHOUT producing fix plan (slug=${failedJob.slug}, code=${exitCode})`);
|
|
1345
|
+
// Record the no-plan outcome so selectAutoFixTargets can offer one
|
|
1346
|
+
// bounded retry instead of permanently dead-ending behind the
|
|
1347
|
+
// 1-attempt cap (autoFixAttempted stays true).
|
|
1348
|
+
mutate((s) => {
|
|
1349
|
+
const j = s.jobs.find((x) => x.slug === failedJob.slug);
|
|
1350
|
+
if (j) j.autoFixOutcome = 'no-plan';
|
|
1351
|
+
}).catch(() => {});
|
|
1332
1352
|
}
|
|
1333
1353
|
// Trigger a tick so the new fix plan is reconciled into the queue and fired.
|
|
1334
1354
|
tickQueue().catch(() => {});
|
|
@@ -1525,17 +1545,16 @@ async function spawnJob(job, runId, runDir, defaultCwd) {
|
|
|
1525
1545
|
// this, the queue stalls indefinitely behind a stale failure even
|
|
1526
1546
|
// though the auto-recovery did its job.
|
|
1527
1547
|
if (effectiveStatus === 'completed' && isFixPlanSlug(job.slug)) {
|
|
1528
|
-
const
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
s.jobs[orig].completedBy = job.slug;
|
|
1548
|
+
const orig = healTargetForFix(job.slug, s.jobs);
|
|
1549
|
+
if (orig) {
|
|
1550
|
+
const priorStatus = orig.status;
|
|
1551
|
+
console.log(`[scheduler] auto-promote: ${orig.slug} (${priorStatus}) → completed because ${job.slug} succeeded`);
|
|
1552
|
+
orig.status = 'completed';
|
|
1553
|
+
orig.exitCode = 0;
|
|
1554
|
+
orig.error = null;
|
|
1555
|
+
orig.completedBy = job.slug;
|
|
1537
1556
|
if (priorStatus === 'needs_review') {
|
|
1538
|
-
delete
|
|
1557
|
+
delete orig.verifierVerdict;
|
|
1539
1558
|
}
|
|
1540
1559
|
}
|
|
1541
1560
|
}
|
|
@@ -1881,16 +1900,57 @@ function selectHistoryJobs(jobs, limit) {
|
|
|
1881
1900
|
// heal a genuinely-unfinished job.
|
|
1882
1901
|
const RESCANNABLE_VERDICTS = new Set(['transcript_errors', 'verify_unavailable']);
|
|
1883
1902
|
|
|
1903
|
+
/**
|
|
1904
|
+
* Backfill a job's missing runId by scanning RUNS_DIR for a run directory
|
|
1905
|
+
* whose contents reference this job's slug (a '<slug>.log' file inside it).
|
|
1906
|
+
* A needs_review job can lose its runId (e.g. an old queue-schema gap) and
|
|
1907
|
+
* become invisible to every auto-resolution path that requires job.runId
|
|
1908
|
+
* truthy. O(number of run dirs) — a single readdir + per-dir existsSync
|
|
1909
|
+
* check, no nested loop over user-scaled data. Dir names are ISO timestamps,
|
|
1910
|
+
* so lexical-descending sort picks the newest match. Exported for tests.
|
|
1911
|
+
*/
|
|
1912
|
+
function resolveRunId(job, { runsDir = RUNS_DIR } = {}) {
|
|
1913
|
+
if (!job || job.runId) return job?.runId || null;
|
|
1914
|
+
if (!job.slug) return null;
|
|
1915
|
+
let dirs;
|
|
1916
|
+
try {
|
|
1917
|
+
dirs = fs.readdirSync(runsDir);
|
|
1918
|
+
} catch {
|
|
1919
|
+
return null;
|
|
1920
|
+
}
|
|
1921
|
+
const matches = dirs.filter((d) => {
|
|
1922
|
+
try {
|
|
1923
|
+
return fs.existsSync(path.join(runsDir, d, `${job.slug}.log`));
|
|
1924
|
+
} catch {
|
|
1925
|
+
return false;
|
|
1926
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
if (!matches.length) return null;
|
|
1929
|
+
matches.sort().reverse();
|
|
1930
|
+
return matches[0];
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
/**
|
|
1934
|
+
* Pure predicate: a needs_review job with no runId and no backfillable run
|
|
1935
|
+
* directory can never self-heal or get an auto-fix investigation — it must
|
|
1936
|
+
* be surfaced for a human rather than silently dead-ended. Exported for
|
|
1937
|
+
* tests.
|
|
1938
|
+
*/
|
|
1939
|
+
function isUnresolvableNeedsReview(job, { hasRunDir }) {
|
|
1940
|
+
return !!job && job.status === 'needs_review' && !job.runId && !hasRunDir;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1884
1943
|
/**
|
|
1885
1944
|
* Pure predicate: is this job eligible for the boot re-verify self-heal? Only
|
|
1886
|
-
* needs_review jobs with a run log
|
|
1887
|
-
* EXCLUDES 'uncommitted_changes' (git
|
|
1888
|
-
* so re-scanning it would falsely
|
|
1945
|
+
* needs_review jobs with a run log (own or backfilled via resolveRunId) AND a
|
|
1946
|
+
* transcript-scan verdict. Crucially EXCLUDES 'uncommitted_changes' (git
|
|
1947
|
+
* commit-guard) — verifyRun can't see git, so re-scanning it would falsely
|
|
1948
|
+
* heal an unfinished job. Exported for tests.
|
|
1889
1949
|
*/
|
|
1890
1950
|
function isRescanCandidate(job) {
|
|
1891
1951
|
return !!job
|
|
1892
1952
|
&& job.status === 'needs_review'
|
|
1893
|
-
&& !!job.runId
|
|
1953
|
+
&& !!(job.runId || resolveRunId(job))
|
|
1894
1954
|
&& RESCANNABLE_VERDICTS.has(job.verifierVerdict);
|
|
1895
1955
|
}
|
|
1896
1956
|
|
|
@@ -1906,23 +1966,32 @@ function isRescanCandidate(job) {
|
|
|
1906
1966
|
* @returns {Promise<{rescanned:number, healed:string[]}>}
|
|
1907
1967
|
*/
|
|
1908
1968
|
/**
|
|
1909
|
-
* Pure helper — no I/O
|
|
1910
|
-
*
|
|
1969
|
+
* Pure helper — no I/O by default (resolveJobRunId is injectable; production
|
|
1970
|
+
* caller passes resolveRunId, which does touch disk). Returns the subset of
|
|
1971
|
+
* jobs eligible for automatic fix-plan authoring after a reverify pass
|
|
1972
|
+
* leaves them still in needs_review.
|
|
1911
1973
|
*
|
|
1912
1974
|
* Exclusion rules (all must pass):
|
|
1913
1975
|
* - status === 'needs_review'
|
|
1914
|
-
* -
|
|
1915
|
-
* - autoFixAttempted !== true (1-attempt cap)
|
|
1976
|
+
* - a runId, own or backfilled via resolveJobRunId (need a run log to investigate)
|
|
1916
1977
|
* - not itself a fix-plan slug (avoids infinite recursion)
|
|
1978
|
+
* - autoFixAttempted cap: a job with no prior attempt is eligible; a job
|
|
1979
|
+
* whose prior attempt outcome was 'no-plan' gets ONE bounded retry
|
|
1980
|
+
* (autoFixRetries < 1); any other attempted job (or an exhausted
|
|
1981
|
+
* no-plan retry) is excluded
|
|
1917
1982
|
* - no fix sibling on disk (fixSlugExists) or already in the queue
|
|
1918
1983
|
*/
|
|
1919
|
-
function selectAutoFixTargets(jobs, { fixSlugExists }) {
|
|
1984
|
+
function selectAutoFixTargets(jobs, { fixSlugExists, resolveJobRunId = resolveRunId }) {
|
|
1920
1985
|
const slugsInQueue = new Set(jobs.map((j) => j.slug));
|
|
1921
1986
|
return jobs.filter((job) => {
|
|
1922
1987
|
if (job.status !== 'needs_review') return false;
|
|
1923
|
-
|
|
1924
|
-
if (
|
|
1988
|
+
const runId = job.runId || resolveJobRunId(job);
|
|
1989
|
+
if (!runId) return false;
|
|
1925
1990
|
if (isFixPlanSlug(job.slug)) return false;
|
|
1991
|
+
if (job.autoFixAttempted) {
|
|
1992
|
+
if (job.autoFixOutcome !== 'no-plan') return false;
|
|
1993
|
+
if ((job.autoFixRetries ?? 0) >= 1) return false;
|
|
1994
|
+
}
|
|
1926
1995
|
const fixSlug = `${String(job.parallelGroup ?? 99).padStart(2, '0')}-fix-${job.slug.replace(/^\d+-/, '')}`;
|
|
1927
1996
|
if (fixSlugExists(fixSlug)) return false;
|
|
1928
1997
|
if (slugsInQueue.has(fixSlug)) return false;
|
|
@@ -1936,7 +2005,7 @@ async function reverifyNeedsReview() {
|
|
|
1936
2005
|
const healed = [];
|
|
1937
2006
|
const leftForReview = [];
|
|
1938
2007
|
for (const job of candidates) {
|
|
1939
|
-
const runDir = path.join(RUNS_DIR, job.runId);
|
|
2008
|
+
const runDir = path.join(RUNS_DIR, job.runId || resolveRunId(job));
|
|
1940
2009
|
const prdPath = path.join(PRDS_DIR, `${job.slug}.md`);
|
|
1941
2010
|
// Derive committedDuringRun from the recorded run window. The live
|
|
1942
2011
|
// commit-guard uses gitHead() (before/after HEAD diff); here the run is
|
|
@@ -1978,24 +2047,72 @@ async function reverifyNeedsReview() {
|
|
|
1978
2047
|
console.log(`[scheduler] boot reverify: left for review: ${detail}`);
|
|
1979
2048
|
}
|
|
1980
2049
|
|
|
2050
|
+
// Surface needs_review jobs that can never self-heal or get an auto-fix
|
|
2051
|
+
// investigation (no runId, no backfillable run dir) instead of leaving
|
|
2052
|
+
// them silently stranded. Also surface no-plan auto-fix jobs whose one
|
|
2053
|
+
// bounded retry is already exhausted. Both are annotated, never looped on.
|
|
2054
|
+
const afterHealForAnnotate = await readQueue();
|
|
2055
|
+
const unresolvable = afterHealForAnnotate.jobs.filter(
|
|
2056
|
+
(j) => isUnresolvableNeedsReview(j, { hasRunDir: !!resolveRunId(j) }) && j.verifierVerdict !== 'no_run_artifacts',
|
|
2057
|
+
);
|
|
2058
|
+
const exhaustedNoPlan = afterHealForAnnotate.jobs.filter(
|
|
2059
|
+
(j) => j.status === 'needs_review'
|
|
2060
|
+
&& j.autoFixAttempted && j.autoFixOutcome === 'no-plan' && (j.autoFixRetries ?? 0) >= 1
|
|
2061
|
+
&& j.verifierVerdict !== 'autofix_no_plan',
|
|
2062
|
+
);
|
|
2063
|
+
if (unresolvable.length || exhaustedNoPlan.length) {
|
|
2064
|
+
const unresolvableSet = new Set(unresolvable.map((j) => j.slug));
|
|
2065
|
+
const exhaustedSet = new Set(exhaustedNoPlan.map((j) => j.slug));
|
|
2066
|
+
await mutate((s) => {
|
|
2067
|
+
for (const j of s.jobs) {
|
|
2068
|
+
if (unresolvableSet.has(j.slug)) {
|
|
2069
|
+
j.verifierVerdict = 'no_run_artifacts';
|
|
2070
|
+
j.error = 'no run artifacts — manual review';
|
|
2071
|
+
} else if (exhaustedSet.has(j.slug)) {
|
|
2072
|
+
j.verifierVerdict = 'autofix_no_plan';
|
|
2073
|
+
j.error = 'auto-fix investigation produced no fix plan after retry — manual review';
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
});
|
|
2077
|
+
if (unresolvable.length) {
|
|
2078
|
+
console.log(`[scheduler] boot reverify: no run artifacts for ${unresolvable.map((j) => j.slug).join(', ')} — flagged for manual review`);
|
|
2079
|
+
}
|
|
2080
|
+
if (exhaustedNoPlan.length) {
|
|
2081
|
+
console.log(`[scheduler] boot reverify: auto-fix retry exhausted for ${exhaustedNoPlan.map((j) => j.slug).join(', ')} — flagged for manual review`);
|
|
2082
|
+
}
|
|
2083
|
+
await broadcast();
|
|
2084
|
+
}
|
|
2085
|
+
|
|
1981
2086
|
// Auto-fix: spawn a fix-plan investigation for each job still in
|
|
1982
2087
|
// needs_review after the heal pass (kill-switch: SM_AUTOFIX_DISABLE=1).
|
|
2088
|
+
// spawnInvestigation early-returns once investigationsInFlight reaches
|
|
2089
|
+
// MAX_CONCURRENT_INVESTIGATIONS (queues the rest for retry), so this loop
|
|
2090
|
+
// cannot fan out past the cap regardless of how many targets are selected.
|
|
1983
2091
|
if (process.env.SM_AUTOFIX_DISABLE !== '1') {
|
|
1984
2092
|
const afterHeal = await readQueue();
|
|
1985
2093
|
const targets = selectAutoFixTargets(afterHeal.jobs, {
|
|
1986
2094
|
fixSlugExists: (s) => fs.existsSync(path.join(PRDS_DIR, `${s}.md`)),
|
|
1987
2095
|
});
|
|
1988
2096
|
for (const job of targets) {
|
|
1989
|
-
const
|
|
2097
|
+
const runId = job.runId || resolveRunId(job);
|
|
2098
|
+
const runDir = path.join(RUNS_DIR, runId);
|
|
2099
|
+
const isNoPlanRetry = job.autoFixAttempted && job.autoFixOutcome === 'no-plan';
|
|
1990
2100
|
// Persist the attempt BEFORE spawning — a crash mid-investigation still
|
|
1991
2101
|
// counts it (mirrors orphanRetries). Safe even when the slot is busy: the
|
|
1992
2102
|
// investigation is queued and drained as slots free, so it is genuinely
|
|
1993
2103
|
// attempted rather than silently dropped.
|
|
1994
2104
|
await mutate((s) => {
|
|
1995
2105
|
const j = s.jobs.find((x) => x.slug === job.slug);
|
|
1996
|
-
if (j)
|
|
2106
|
+
if (j) {
|
|
2107
|
+
j.autoFixAttempted = true;
|
|
2108
|
+
if (!j.runId && runId) j.runId = runId;
|
|
2109
|
+
if (isNoPlanRetry) {
|
|
2110
|
+
j.autoFixRetries = (j.autoFixRetries ?? 0) + 1;
|
|
2111
|
+
delete j.autoFixOutcome;
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
1997
2114
|
});
|
|
1998
|
-
console.log(`[scheduler] auto-fix: needs_review ${job.slug} → authoring fix-plan (1/1)`);
|
|
2115
|
+
console.log(`[scheduler] auto-fix: needs_review ${job.slug} → authoring fix-plan (${isNoPlanRetry ? 'retry' : '1/1'})`);
|
|
1999
2116
|
spawnInvestigation(job, runDir).catch((e) => {
|
|
2000
2117
|
console.error('[scheduler] auto-fix spawnInvestigation error', job.slug, e);
|
|
2001
2118
|
});
|
|
@@ -2335,10 +2452,16 @@ async function init() {
|
|
|
2335
2452
|
// Periodic self-heal: re-run the verifier over stale needs_review jobs so a
|
|
2336
2453
|
// job whose work actually landed (committed in-window, no FAIL sentinel)
|
|
2337
2454
|
// auto-clears WITHOUT waiting for the next app restart. Cheap-guarded — the
|
|
2338
|
-
// log scan only runs when something is actually flagged.
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2455
|
+
// log scan only runs when something is actually flagged. Kill-switch:
|
|
2456
|
+
// SM_REVERIFY_PERIODIC_DISABLE=1 (boot reverify above stays always-on).
|
|
2457
|
+
// reverifyNeedsReview's auto-fix loop is capped downstream by
|
|
2458
|
+
// MAX_CONCURRENT_INVESTIGATIONS (spawnInvestigation queues/early-returns
|
|
2459
|
+
// past it), so this interval firing cannot fan out investigations.
|
|
2460
|
+
if (process.env.SM_REVERIFY_PERIODIC_DISABLE !== '1') {
|
|
2461
|
+
const s = readQueueSync();
|
|
2462
|
+
if (s.jobs.some((j) => j.status === 'needs_review')) {
|
|
2463
|
+
reverifyNeedsReview().catch(() => {});
|
|
2464
|
+
}
|
|
2342
2465
|
}
|
|
2343
2466
|
}, 10 * 60_000);
|
|
2344
2467
|
|
|
@@ -2479,7 +2602,12 @@ const remote = {
|
|
|
2479
2602
|
});
|
|
2480
2603
|
if (!found) return { ok: false, error: 'not found' };
|
|
2481
2604
|
await broadcast();
|
|
2482
|
-
return { ok: true };
|
|
2605
|
+
return { ok: true, slug, status: 'pending' };
|
|
2606
|
+
},
|
|
2607
|
+
|
|
2608
|
+
async listJobs() {
|
|
2609
|
+
const state = await readQueue();
|
|
2610
|
+
return state.jobs.map((j) => ({ slug: j.slug, title: j.title, status: j.status, cwd: j.cwd }));
|
|
2483
2611
|
},
|
|
2484
2612
|
|
|
2485
2613
|
async runNow() {
|
|
@@ -2505,4 +2633,4 @@ const remote = {
|
|
|
2505
2633
|
},
|
|
2506
2634
|
};
|
|
2507
2635
|
|
|
2508
|
-
module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets };
|
|
2636
|
+
module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets, resolveRunId, isUnresolvableNeedsReview, healTargetForFix };
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* Storage: ~/.config/session-manager/tabs.json
|
|
6
6
|
* Shape: { tabs: PersistedTab[], activeTabId: string | null, savedAt: number }
|
|
7
7
|
*
|
|
8
|
-
* Only serializable, durable fields are persisted: id, claudeSessionId,
|
|
9
|
-
* label, presetId. Runtime-only fields (pid, status,
|
|
10
|
-
* exitCode) are recomputed on boot.
|
|
8
|
+
* Only serializable, durable fields are persisted: id, claudeSessionId,
|
|
9
|
+
* chatSessionId, cwd, label, presetId. Runtime-only fields (pid, status,
|
|
10
|
+
* startupCommand, exitCode) are recomputed on boot.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
const fs = require('node:fs');
|
|
@@ -59,6 +59,7 @@ async function markFreshRestart() {
|
|
|
59
59
|
const freshTabs = tabs.map((t) => ({
|
|
60
60
|
...t,
|
|
61
61
|
claudeSessionId: crypto.randomUUID(),
|
|
62
|
+
chatSessionId: crypto.randomUUID(),
|
|
62
63
|
}));
|
|
63
64
|
await save({ tabs: freshTabs, activeTabId, freshStart: true });
|
|
64
65
|
}
|
package/src/preload/api.d.ts
CHANGED
|
@@ -15,6 +15,72 @@ export interface WriteErrorEvent {
|
|
|
15
15
|
reason: string;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export interface BrowserNavState {
|
|
19
|
+
url: string;
|
|
20
|
+
title: string;
|
|
21
|
+
canGoBack: boolean;
|
|
22
|
+
canGoForward: boolean;
|
|
23
|
+
loading: boolean;
|
|
24
|
+
isSecure: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** One captured recorder step (PRD 408 engine → PRD 409 panel). */
|
|
28
|
+
export interface RecordStep {
|
|
29
|
+
n: number;
|
|
30
|
+
/** `select` is accepted by replay/export (PRD 410) for forward-compat;
|
|
31
|
+
* the live engine does not emit it yet. */
|
|
32
|
+
verb: 'navigate' | 'click' | 'type' | 'select' | 'wait-for';
|
|
33
|
+
target: string;
|
|
34
|
+
kind?: 'nav' | 'assert';
|
|
35
|
+
/** True for `type` steps — the actual typed value is never captured. */
|
|
36
|
+
masked?: boolean;
|
|
37
|
+
/** Engine-suggested `{{var}}` name for a `type` step (e.g. field name). */
|
|
38
|
+
variableSuggestion?: string;
|
|
39
|
+
/** Renderer-owned: set once the user checks "parameterize as {{var}}". */
|
|
40
|
+
variable?: string | null;
|
|
41
|
+
/** `select` steps only — the option value to choose on replay/export. */
|
|
42
|
+
value?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Per-step replay outcome (PRD 410), streamed as `browser:replay-step:<viewId>`. */
|
|
46
|
+
export interface ReplayStepResult {
|
|
47
|
+
n: number;
|
|
48
|
+
status: 'pass' | 'fail';
|
|
49
|
+
detail?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** find-in-page result (PRD 402), streamed as `browser:find-result:<viewId>`. */
|
|
53
|
+
export interface FindResult {
|
|
54
|
+
requestId: number;
|
|
55
|
+
matches: number;
|
|
56
|
+
activeMatchOrdinal: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Element-picker event (PRD 403), streamed as `browser:picker-event:<viewId>`. */
|
|
60
|
+
export interface PickerEvent {
|
|
61
|
+
type: 'hover' | 'pick' | 'unpick' | 'exit';
|
|
62
|
+
selector?: string;
|
|
63
|
+
label?: string;
|
|
64
|
+
tag?: string;
|
|
65
|
+
rect?: { x: number; y: number; width: number; height: number };
|
|
66
|
+
/** Present on a 'pick' event fired by an unmodified click — the selection was replaced, not accumulated. */
|
|
67
|
+
replace?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** One entry in `~/.claude/session-manager/browser/history.json` (PRD 402). */
|
|
71
|
+
export interface BrowserHistoryEntry {
|
|
72
|
+
url: string;
|
|
73
|
+
title: string;
|
|
74
|
+
ts: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** One entry in `~/.claude/session-manager/browser/bookmarks.json` (PRD 402). */
|
|
78
|
+
export interface BrowserBookmark {
|
|
79
|
+
url: string;
|
|
80
|
+
title: string;
|
|
81
|
+
ts: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
18
84
|
export interface ReadJsonResult {
|
|
19
85
|
exists: boolean;
|
|
20
86
|
raw: string;
|
|
@@ -82,6 +148,8 @@ export interface SubscribeResult {
|
|
|
82
148
|
export interface PersistedTab {
|
|
83
149
|
id: string;
|
|
84
150
|
claudeSessionId: string;
|
|
151
|
+
/** Chat mode's own session id, decoupled from claudeSessionId. Optional for backwards-compat with tabs.json written before this field existed. */
|
|
152
|
+
chatSessionId?: string;
|
|
85
153
|
cwd: string;
|
|
86
154
|
label: string;
|
|
87
155
|
presetId: string | null;
|
|
@@ -130,6 +198,21 @@ export type BillingFetchResult =
|
|
|
130
198
|
| { kind: 'transient'; message: string; httpStatus: number | null }
|
|
131
199
|
| { kind: 'config'; message: string };
|
|
132
200
|
|
|
201
|
+
// ── MCP server live connection probe (`claude mcp list`)
|
|
202
|
+
export interface McpServerStatus {
|
|
203
|
+
name: string;
|
|
204
|
+
target: string;
|
|
205
|
+
transport: 'stdio' | 'http' | 'sse' | 'ws' | 'unknown';
|
|
206
|
+
status: 'connected' | 'failed' | 'needs-auth' | 'pending' | 'unknown';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface McpStatusResult {
|
|
210
|
+
ok: boolean;
|
|
211
|
+
servers: McpServerStatus[];
|
|
212
|
+
error?: string;
|
|
213
|
+
checkedAt: number;
|
|
214
|
+
}
|
|
215
|
+
|
|
133
216
|
// ── Usage matrix (AgOps dashboard, main-side aggregator)
|
|
134
217
|
export type UsageMatrixState = 'idle' | 'executing' | 'plan' | 'background';
|
|
135
218
|
export type UsageMatrixIntensity = 'idle' | 'low' | 'medium' | 'critical';
|
|
@@ -435,12 +518,23 @@ export interface DayProjectRow {
|
|
|
435
518
|
sessionCount: number;
|
|
436
519
|
errorCount: number;
|
|
437
520
|
estimatedCostUsd: number;
|
|
521
|
+
byModel: Record<string, {
|
|
522
|
+
inputTokens: number;
|
|
523
|
+
outputTokens: number;
|
|
524
|
+
cacheReadTokens: number;
|
|
525
|
+
cacheCreationTokens: number;
|
|
526
|
+
costUsd: number;
|
|
527
|
+
/** Set when the model id didn't match opus/sonnet/haiku and was priced at Sonnet rates as a fallback. */
|
|
528
|
+
estimated?: boolean;
|
|
529
|
+
}>;
|
|
438
530
|
}
|
|
439
531
|
|
|
440
532
|
export interface HistoryAggregateResult {
|
|
441
533
|
rows: DayProjectRow[];
|
|
442
534
|
partial: boolean;
|
|
443
535
|
scannedMs: number;
|
|
536
|
+
/** Total $ saved across all rows from cache-read pricing vs. full input pricing. Global, not per-row. */
|
|
537
|
+
cacheSavingsUsd: number;
|
|
444
538
|
}
|
|
445
539
|
|
|
446
540
|
export interface SessionScanEntry {
|
|
@@ -899,6 +993,12 @@ export interface ChatRunErrorEvent {
|
|
|
899
993
|
message: string;
|
|
900
994
|
}
|
|
901
995
|
|
|
996
|
+
export interface ChatRunNoticeEvent {
|
|
997
|
+
tabId: string;
|
|
998
|
+
sessionId: string;
|
|
999
|
+
message: string;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
902
1002
|
export interface SessionManagerAPI {
|
|
903
1003
|
app: {
|
|
904
1004
|
version: () => Promise<string>;
|
|
@@ -932,6 +1032,54 @@ export interface SessionManagerAPI {
|
|
|
932
1032
|
onExit: (tabId: string, handler: (info: PtyExit) => void) => () => void;
|
|
933
1033
|
onWriteError: (handler: (ev: WriteErrorEvent) => void) => () => void;
|
|
934
1034
|
};
|
|
1035
|
+
browser: {
|
|
1036
|
+
create: (payload: { viewId: string; partition: string }) => Promise<{ ok: boolean }>;
|
|
1037
|
+
setBounds: (payload: { viewId: string; x: number; y: number; width: number; height: number }) => Promise<{ ok: boolean }>;
|
|
1038
|
+
show: (viewId: string) => Promise<{ ok: boolean }>;
|
|
1039
|
+
hide: (viewId: string) => Promise<{ ok: boolean }>;
|
|
1040
|
+
destroy: (viewId: string) => Promise<{ ok: boolean }>;
|
|
1041
|
+
navigate: (payload: { viewId: string; url: string }) => Promise<{ ok: boolean; error?: string }>;
|
|
1042
|
+
back: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1043
|
+
forward: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1044
|
+
reload: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1045
|
+
stop: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1046
|
+
onNavState: (viewId: string, handler: (state: BrowserNavState) => void) => () => void;
|
|
1047
|
+
recordStart: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1048
|
+
recordStop: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1049
|
+
onRecordStep: (viewId: string, handler: (step: RecordStep) => void) => () => void;
|
|
1050
|
+
captureDom: (payload: { viewId: string; kind: 'text' | 'html' }) => Promise<
|
|
1051
|
+
| { ok: true; url: string; title: string; text: string; truncated?: boolean }
|
|
1052
|
+
| { ok: false; error: string }
|
|
1053
|
+
>;
|
|
1054
|
+
captureShot: (viewId: string) => Promise<
|
|
1055
|
+
| { ok: true; url: string; title: string; dataUrl: string }
|
|
1056
|
+
| { ok: false; error: string }
|
|
1057
|
+
>;
|
|
1058
|
+
saveBinary: (path: string, base64: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1059
|
+
replay: (payload: {
|
|
1060
|
+
viewId: string;
|
|
1061
|
+
steps: RecordStep[];
|
|
1062
|
+
values?: Record<string, string>;
|
|
1063
|
+
continueOnError?: boolean;
|
|
1064
|
+
}) => Promise<{ ok: boolean; error?: string; stopped?: boolean; failedAt?: number }>;
|
|
1065
|
+
onReplayStep: (viewId: string, handler: (step: ReplayStepResult) => void) => () => void;
|
|
1066
|
+
setZoom: (payload: { viewId: string; factor: number }) => Promise<{ ok: boolean; error?: string; factor?: number }>;
|
|
1067
|
+
find: (payload: { viewId: string; text: string; forward?: boolean }) => Promise<{ ok: boolean; error?: string }>;
|
|
1068
|
+
stopFind: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1069
|
+
onFindResult: (viewId: string, handler: (result: FindResult) => void) => () => void;
|
|
1070
|
+
pickerStart: (viewId: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1071
|
+
pickerStop: (viewId: string) => Promise<{ ok: boolean; error?: string; wasPicking?: boolean }>;
|
|
1072
|
+
onPickerEvent: (viewId: string, handler: (ev: PickerEvent) => void) => () => void;
|
|
1073
|
+
/** Selection-scoped capture pipeline (PRD 404): filter -> prune -> summarize -> chunk for 'agent', outerHTML for 'html', CDP AX-tree for 'a11y', fallback-chain for 'selector'. */
|
|
1074
|
+
capture: (payload: {
|
|
1075
|
+
viewId: string;
|
|
1076
|
+
selectors: string[];
|
|
1077
|
+
mode: 'agent' | 'html' | 'a11y' | 'selector';
|
|
1078
|
+
}) => Promise<
|
|
1079
|
+
| { ok: true; mode: string; text: string; meta: { chunks?: number; tokens?: number } }
|
|
1080
|
+
| { ok: false; error: string }
|
|
1081
|
+
>;
|
|
1082
|
+
};
|
|
935
1083
|
transcripts: {
|
|
936
1084
|
subscribe: (payload: { tabId: string; cwd: string; sessionUuid: string }) => Promise<SubscribeResult>;
|
|
937
1085
|
/** Release the sub back to the LRU cache (view-switch). Does not destroy the watcher. */
|
|
@@ -949,6 +1097,9 @@ export interface SessionManagerAPI {
|
|
|
949
1097
|
billing: {
|
|
950
1098
|
fetch: () => Promise<BillingFetchResult>;
|
|
951
1099
|
};
|
|
1100
|
+
mcp: {
|
|
1101
|
+
status: () => Promise<McpStatusResult>;
|
|
1102
|
+
};
|
|
952
1103
|
usageMatrix: {
|
|
953
1104
|
snapshot: () => Promise<UsageMatrixSnapshot>;
|
|
954
1105
|
onTick: (handler: (snap: UsageMatrixSnapshot) => void) => () => void;
|
|
@@ -1099,6 +1250,8 @@ export interface SessionManagerAPI {
|
|
|
1099
1250
|
| { ok: true; path: string; bytes: number }
|
|
1100
1251
|
| { ok: false; empty?: true; error?: string }
|
|
1101
1252
|
>;
|
|
1253
|
+
/** Write side — copies a Capture-panel screenshot data URL to the OS clipboard. */
|
|
1254
|
+
copyImage: (dataUrl: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1102
1255
|
};
|
|
1103
1256
|
memory: {
|
|
1104
1257
|
/** List markdown memory entries for the given workspace (defaults to 'default'). */
|
|
@@ -1129,11 +1282,6 @@ export interface SessionManagerAPI {
|
|
|
1129
1282
|
/** Delete one entry. Removes the file outright when last entry is removed. */
|
|
1130
1283
|
delete: (agentId: string, entryId: string) => Promise<AgentMemoryMutationResult>;
|
|
1131
1284
|
};
|
|
1132
|
-
docEditor: {
|
|
1133
|
-
pickFile: (payload?: { lastDir?: string }) => Promise<{ path: string | null; error?: string }>;
|
|
1134
|
-
readFile: (path: string) => Promise<{ ok: boolean; text?: string; mtimeMs?: number; error?: string }>;
|
|
1135
|
-
writeFile: (path: string, text: string) => Promise<{ ok: boolean; mtimeMs?: number; error?: string }>;
|
|
1136
|
-
};
|
|
1137
1285
|
git: {
|
|
1138
1286
|
/** Full git status for `cwd`. Returns null when not a git repo, git is
|
|
1139
1287
|
* missing, or the call times out (5s ceiling). Cached per-cwd for 5s. */
|
|
@@ -1187,6 +1335,7 @@ export interface SessionManagerAPI {
|
|
|
1187
1335
|
onNeedsInput: (handler: (e: ChatRunNeedsInputEvent) => void) => () => void;
|
|
1188
1336
|
onComplete: (handler: (e: ChatRunCompleteEvent) => void) => () => void;
|
|
1189
1337
|
onError: (handler: (e: ChatRunErrorEvent) => void) => () => void;
|
|
1338
|
+
onNotice: (handler: (e: ChatRunNoticeEvent) => void) => () => void;
|
|
1190
1339
|
};
|
|
1191
1340
|
exchanges: {
|
|
1192
1341
|
/** Durable per-exchange log entries for a project, newest-first (max 100 by default). */
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preload for embedded Browser-tab WebContentsViews (PRD 408 recorder
|
|
3
|
+
* engine). Exposes a minimal `window.__smRecorder.setRecording(bool)` toggle
|
|
4
|
+
* via contextBridge so the main process can arm/disarm step capture without
|
|
5
|
+
* ever giving page script node/ipcRenderer access. Typed values are never
|
|
6
|
+
* captured — only the target selector + a `masked` flag — per the tab's
|
|
7
|
+
* "sandboxed · no filesystem · no passwords" guardrail.
|
|
8
|
+
*/
|
|
9
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
10
|
+
|
|
11
|
+
// Secret handed to us via `additionalArguments` (never touches the DOM or a
|
|
12
|
+
// script tag the embedded page could inspect) — required on every toggle so
|
|
13
|
+
// the page itself can't call the exposeInMainWorld bridge to interfere with
|
|
14
|
+
// a recording session it has no business controlling.
|
|
15
|
+
const TOKEN_PREFIX = '--sm-record-token=';
|
|
16
|
+
const expectedToken = (process.argv.find((a) => a.startsWith(TOKEN_PREFIX)) || '').slice(TOKEN_PREFIX.length) || null;
|
|
17
|
+
|
|
18
|
+
let capturing = false;
|
|
19
|
+
|
|
20
|
+
function selectorFor(el) {
|
|
21
|
+
if (!el || el.nodeType !== 1) return '';
|
|
22
|
+
if (el.id) return `#${el.id}`;
|
|
23
|
+
const testId = typeof el.getAttribute === 'function' ? el.getAttribute('data-testid') : null;
|
|
24
|
+
if (testId) return `[data-testid="${testId}"]`;
|
|
25
|
+
const tag = el.tagName.toLowerCase();
|
|
26
|
+
const cls = typeof el.className === 'string' ? el.className.trim().split(/\s+/)[0] : '';
|
|
27
|
+
return cls ? `${tag}.${cls}` : tag;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function variableSuggestionFor(el) {
|
|
31
|
+
const name = String(el.name || el.id || '').toLowerCase();
|
|
32
|
+
if (el.type === 'password' || /pass/.test(name)) return 'password';
|
|
33
|
+
if (/email/.test(name)) return 'email';
|
|
34
|
+
if (/user/.test(name)) return 'username';
|
|
35
|
+
return name || 'value';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
document.addEventListener(
|
|
39
|
+
'click',
|
|
40
|
+
(e) => {
|
|
41
|
+
if (!capturing) return;
|
|
42
|
+
ipcRenderer.send('browser:record-event', { verb: 'click', target: selectorFor(e.target) });
|
|
43
|
+
},
|
|
44
|
+
true,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
document.addEventListener(
|
|
48
|
+
'change',
|
|
49
|
+
(e) => {
|
|
50
|
+
if (!capturing) return;
|
|
51
|
+
const el = e.target;
|
|
52
|
+
if (!el || (el.tagName !== 'INPUT' && el.tagName !== 'TEXTAREA')) return;
|
|
53
|
+
ipcRenderer.send('browser:record-event', {
|
|
54
|
+
verb: 'type',
|
|
55
|
+
target: selectorFor(el),
|
|
56
|
+
variableSuggestion: variableSuggestionFor(el),
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
true,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
contextBridge.exposeInMainWorld('__smRecorder', {
|
|
63
|
+
setRecording: (v, token) => {
|
|
64
|
+
if (!expectedToken || token !== expectedToken) return;
|
|
65
|
+
capturing = !!v;
|
|
66
|
+
},
|
|
67
|
+
});
|