claude-code-session-manager 0.35.18 → 0.37.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.
Files changed (39) hide show
  1. package/dist/assets/{TiptapBody-DgFO_m3g.js → TiptapBody-Cg8YZhVZ.js} +58 -58
  2. package/dist/assets/index-CTTjT08J.css +32 -0
  3. package/dist/assets/index-_iBXLuvt.js +3496 -0
  4. package/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/plugins/session-manager-dev/skills/develop/SKILL.md +5 -1
  7. package/src/main/__tests__/broadcastCoalescer.test.cjs +104 -0
  8. package/src/main/__tests__/docEdit.test.cjs +82 -0
  9. package/src/main/__tests__/historyDashboard.test.cjs +163 -0
  10. package/src/main/__tests__/historyRollup.test.cjs +333 -0
  11. package/src/main/__tests__/memoryStale.test.cjs +88 -0
  12. package/src/main/__tests__/prdParserHighWater.test.cjs +74 -0
  13. package/src/main/__tests__/queueHistory.test.cjs +233 -0
  14. package/src/main/__tests__/queueOpsAutoArchive.test.cjs +142 -0
  15. package/src/main/__tests__/rcaFeedbackHook.test.cjs +259 -0
  16. package/src/main/__tests__/scheduler-committed-in-window.test.cjs +4 -5
  17. package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +51 -0
  18. package/src/main/chatRunner.cjs +57 -18
  19. package/src/main/config.cjs +8 -0
  20. package/src/main/docEdit.cjs +133 -0
  21. package/src/main/files.cjs +53 -0
  22. package/src/main/historyAggregator.cjs +391 -50
  23. package/src/main/historyDashboard.cjs +226 -0
  24. package/src/main/index.cjs +37 -1
  25. package/src/main/ipcSchemas.cjs +29 -0
  26. package/src/main/lib/broadcastCoalescer.cjs +46 -0
  27. package/src/main/lib/historyRollup.cjs +148 -0
  28. package/src/main/lib/memoryStale.cjs +116 -0
  29. package/src/main/lib/queueHistory.cjs +216 -0
  30. package/src/main/lib/rcaFeedbackHook.cjs +346 -0
  31. package/src/main/lib/schedulerConfig.cjs +16 -0
  32. package/src/main/memoryTool.cjs +49 -0
  33. package/src/main/queueOps.cjs +92 -0
  34. package/src/main/scheduler/prdParser.cjs +52 -1
  35. package/src/main/scheduler.cjs +237 -29
  36. package/src/preload/api.d.ts +92 -1
  37. package/src/preload/index.cjs +7 -0
  38. package/dist/assets/index-4dJkn9nT.js +0 -3559
  39. package/dist/assets/index-DX2w2YhJ.css +0 -32
package/dist/index.html CHANGED
@@ -7,10 +7,10 @@
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
9
  <link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,400&family=Geist:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
10
- <script type="module" crossorigin src="./assets/index-4dJkn9nT.js"></script>
10
+ <script type="module" crossorigin src="./assets/index-_iBXLuvt.js"></script>
11
11
  <link rel="modulepreload" crossorigin href="./assets/monaco-editor-BW5C4Iv1.js">
12
12
  <link rel="stylesheet" crossorigin href="./assets/monaco-editor-BTnBOi8r.css">
13
- <link rel="stylesheet" crossorigin href="./assets/index-DX2w2YhJ.css">
13
+ <link rel="stylesheet" crossorigin href="./assets/index-CTTjT08J.css">
14
14
  </head>
15
15
  <body class="bg-bg text-fg font-sans antialiased">
16
16
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-session-manager",
3
- "version": "0.35.18",
3
+ "version": "0.37.0",
4
4
  "description": "Local cockpit for the Claude Code CLI — multi-tab terminal, full config surface, scheduler, voice dictation, and live observability.",
5
5
  "type": "module",
6
6
  "main": "src/main/index.cjs",
@@ -189,7 +189,11 @@ single definition of "tracked to done" for both entry paths.
189
189
  - **Failed / errored / `needs_review` / timed out / killed by the watchdog or supervisor /
190
190
  overran its estimate badly** — STOP waiting and surface it now: which PRD, the failure
191
191
  signal, the relevant log tail, and the likely cause (a stuck poll-loop or post-AC overrun
192
- per `PRD_AUTHORING.md`). Don't silently retry forever. A `rateLimited` exit-1 is the
192
+ per `PRD_AUTHORING.md`). Don't silently retry forever. For `needs_review`, the scheduler
193
+ auto-files a Root Cause Analysis into the target project's feedback inbox
194
+ (`rcaFeedbackHook`, filename `<date>-rca-<slug>-<runId>.md`) — reference that file in
195
+ your report rather than re-deriving the analysis, and let `/process-feedback` fold its
196
+ prevention hint back into future PRD authoring. A `rateLimited` exit-1 is the
193
197
  scheduler's benign auto-pause (auto-resumes next window) — keep waiting, don't escalate.
194
198
  - **All PRDs completed successfully** — go to step 7.
195
199
 
@@ -0,0 +1,104 @@
1
+ /**
2
+ * broadcastCoalescer.test.cjs — unit tests for src/main/lib/broadcastCoalescer.cjs,
3
+ * the trailing-edge debounce used by scheduler.cjs's broadcast().
4
+ *
5
+ * Run: timeout 300 npx vitest run src/main/__tests__/broadcastCoalescer.test.cjs
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ import { test, expect, vi, beforeEach, afterEach } from 'vitest';
11
+
12
+ const { createBroadcastCoalescer } = require('../lib/broadcastCoalescer.cjs');
13
+
14
+ beforeEach(() => {
15
+ vi.useFakeTimers();
16
+ });
17
+
18
+ afterEach(() => {
19
+ vi.useRealTimers();
20
+ });
21
+
22
+ function makeCoalescer(delayMs = 200) {
23
+ const sent = [];
24
+ let counter = 0;
25
+ const getPayload = vi.fn(() => `state-${counter}`);
26
+ const send = vi.fn((payload) => sent.push(payload));
27
+ const coalescer = createBroadcastCoalescer({ delayMs, send, getPayload });
28
+ return {
29
+ coalescer,
30
+ sent,
31
+ send,
32
+ getPayload,
33
+ bump: () => { counter += 1; },
34
+ };
35
+ }
36
+
37
+ test('5 rapid schedule() calls within the window produce exactly 1 send', async () => {
38
+ const { coalescer, sent, bump } = makeCoalescer(200);
39
+
40
+ bump(); coalescer.schedule();
41
+ bump(); coalescer.schedule();
42
+ bump(); coalescer.schedule();
43
+ bump(); coalescer.schedule();
44
+ bump(); coalescer.schedule();
45
+
46
+ await vi.advanceTimersByTimeAsync(200);
47
+
48
+ expect(sent.length).toBe(1);
49
+ });
50
+
51
+ test('the trailing send reflects the LATEST state, not the state at the first schedule() call', async () => {
52
+ const { coalescer, sent, bump } = makeCoalescer(200);
53
+
54
+ bump(); // state-1
55
+ coalescer.schedule();
56
+ bump(); // state-2
57
+ coalescer.schedule(); // no-op — timer already armed
58
+ bump(); // state-3
59
+ coalescer.schedule(); // no-op
60
+
61
+ await vi.advanceTimersByTimeAsync(200);
62
+
63
+ expect(sent).toEqual(['state-3']);
64
+ });
65
+
66
+ test('flush() cancels the pending timer and sends immediately with the current payload', async () => {
67
+ const { coalescer, sent, bump } = makeCoalescer(200);
68
+
69
+ bump();
70
+ coalescer.schedule();
71
+
72
+ // Flush before the 200ms window elapses.
73
+ await vi.advanceTimersByTimeAsync(50);
74
+ await coalescer.flush();
75
+
76
+ expect(sent).toEqual(['state-1']);
77
+
78
+ // The window's original timer must not fire a second, stale send.
79
+ await vi.advanceTimersByTimeAsync(200);
80
+ expect(sent).toEqual(['state-1']);
81
+ });
82
+
83
+ test('a schedule() call after a quiet period still arrives (not dropped)', async () => {
84
+ const { coalescer, sent, bump } = makeCoalescer(200);
85
+
86
+ bump();
87
+ coalescer.schedule();
88
+ await vi.advanceTimersByTimeAsync(200);
89
+ expect(sent).toEqual(['state-1']);
90
+
91
+ bump();
92
+ coalescer.schedule();
93
+ await vi.advanceTimersByTimeAsync(200);
94
+ expect(sent).toEqual(['state-1', 'state-2']);
95
+ });
96
+
97
+ test('flush() with no pending timer still sends (no-arm case)', async () => {
98
+ const { coalescer, sent, bump } = makeCoalescer(200);
99
+ bump();
100
+ await coalescer.flush();
101
+ expect(sent).toEqual(['state-1']);
102
+ });
103
+
104
+ console.log('broadcastCoalescer tests: PASS');
@@ -0,0 +1,82 @@
1
+ /**
2
+ * docEdit.test.cjs — unit tests for docEdit.cjs's pure helpers (PRD 638).
3
+ *
4
+ * Run: timeout 300 npx vitest run src/main/__tests__/docEdit.test.cjs
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ import { test, expect, describe } from 'vitest';
10
+ const { parseDocEdit } = require('../docEdit.cjs');
11
+ const { duplicateNameFor } = require('../files.cjs');
12
+
13
+ describe('parseDocEdit', () => {
14
+ test('valid JSON', () => {
15
+ const result = parseDocEdit('{"after":"rewritten text"}');
16
+ expect(result).toEqual({ ok: true, after: 'rewritten text' });
17
+ });
18
+
19
+ test('JSON embedded in prose', () => {
20
+ const result = parseDocEdit('Sure, here you go:\n{"after":"rewritten text"}\nHope that helps!');
21
+ expect(result).toEqual({ ok: true, after: 'rewritten text' });
22
+ });
23
+
24
+ test('missing after', () => {
25
+ const result = parseDocEdit('{"other":"value"}');
26
+ expect(result.ok).toBe(false);
27
+ expect(result.error).toBeTruthy();
28
+ });
29
+
30
+ test('empty after', () => {
31
+ const result = parseDocEdit('{"after":""}');
32
+ expect(result.ok).toBe(false);
33
+ expect(result.error).toBeTruthy();
34
+ });
35
+
36
+ test('non-string after', () => {
37
+ const result = parseDocEdit('{"after":42}');
38
+ expect(result.ok).toBe(false);
39
+ expect(result.error).toBeTruthy();
40
+ });
41
+
42
+ test('over-length after', () => {
43
+ const result = parseDocEdit(JSON.stringify({ after: 'x'.repeat(16001) }));
44
+ expect(result.ok).toBe(false);
45
+ expect(result.error).toBeTruthy();
46
+ });
47
+
48
+ test('no JSON object in output at all', () => {
49
+ const result = parseDocEdit('not json at all, just prose garbage');
50
+ expect(result.ok).toBe(false);
51
+ expect(result.error).toBeTruthy();
52
+ });
53
+
54
+ test('empty/null input', () => {
55
+ expect(parseDocEdit('').ok).toBe(false);
56
+ expect(parseDocEdit(null).ok).toBe(false);
57
+ });
58
+ });
59
+
60
+ describe('duplicateNameFor', () => {
61
+ test('fresh name — no collision', () => {
62
+ const result = duplicateNameFor('/tmp/dir', 'notes.txt', () => false);
63
+ expect(result).toEqual({ ok: true, name: 'notes-copy.txt' });
64
+ });
65
+
66
+ test('collision on the first candidate falls through to -copy-2', () => {
67
+ const taken = new Set(['/tmp/dir/notes-copy.txt']);
68
+ const result = duplicateNameFor('/tmp/dir', 'notes.txt', (full) => taken.has(full));
69
+ expect(result).toEqual({ ok: true, name: 'notes-copy-2.txt' });
70
+ });
71
+
72
+ test('preserves extension-less basenames', () => {
73
+ const result = duplicateNameFor('/tmp/dir', 'README', () => false);
74
+ expect(result).toEqual({ ok: true, name: 'README-copy' });
75
+ });
76
+
77
+ test('cap exhausted after 20 attempts returns an error', () => {
78
+ const result = duplicateNameFor('/tmp/dir', 'notes.txt', () => true);
79
+ expect(result.ok).toBe(false);
80
+ expect(result.error).toBeTruthy();
81
+ });
82
+ });
@@ -0,0 +1,163 @@
1
+ /**
2
+ * historyDashboard.test.cjs — unit tests for src/main/historyDashboard.cjs
3
+ * (the `history:dashboard` IPC's pure rollup-only computation).
4
+ *
5
+ * Run: timeout 300 npx vitest run src/main/__tests__/historyDashboard.test.cjs
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ import { test, expect, beforeEach, afterEach, vi } from 'vitest';
11
+ const fs = require('node:fs');
12
+ const os = require('node:os');
13
+ const path = require('node:path');
14
+
15
+ let tmpHome;
16
+ let realHome;
17
+ let historyRollup;
18
+ let historyDashboard;
19
+
20
+ const MODULES_TO_RELOAD = [
21
+ '../lib/historyRollup.cjs',
22
+ '../historyAggregator.cjs',
23
+ '../historyDashboard.cjs',
24
+ '../config.cjs',
25
+ ];
26
+
27
+ function purgeRequireCache() {
28
+ for (const m of MODULES_TO_RELOAD) {
29
+ try { delete require.cache[require.resolve(m)]; } catch { /* not loaded yet */ }
30
+ }
31
+ }
32
+
33
+ beforeEach(() => {
34
+ realHome = process.env.HOME;
35
+ tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'history-dashboard-test-'));
36
+ process.env.HOME = tmpHome;
37
+ purgeRequireCache();
38
+ historyRollup = require('../lib/historyRollup.cjs');
39
+ historyDashboard = require('../historyDashboard.cjs');
40
+ });
41
+
42
+ afterEach(() => {
43
+ process.env.HOME = realHome;
44
+ purgeRequireCache();
45
+ fs.rmSync(tmpHome, { recursive: true, force: true });
46
+ });
47
+
48
+ function today() {
49
+ return new Date().toLocaleDateString('en-CA');
50
+ }
51
+
52
+ function daysAgo(n) {
53
+ const d = new Date();
54
+ d.setDate(d.getDate() - n);
55
+ return d.toLocaleDateString('en-CA');
56
+ }
57
+
58
+ async function seedThreeDayFixture() {
59
+ const d2 = daysAgo(2);
60
+ const d1 = daysAgo(1);
61
+ const d0 = today();
62
+
63
+ await historyRollup.appendRollupDays([
64
+ // day -2: project A, finalized, sonnet
65
+ { date: d2, projectDir: 'proj-a', modelId: historyRollup.TOTALS_MODEL_ID, promptCount: 4, toolCallCount: 2, toolBreakdown: { Read: 2 }, errorCount: 0, sessionCount: 1, activeMinutes: 12, v: 2 },
66
+ { date: d2, projectDir: 'proj-a', modelId: 'claude-sonnet-5', promptCount: 0, inputTokens: 1000, outputTokens: 200, cacheReadTokens: 0, cacheCreationTokens: 0, v: 2 },
67
+ { date: d2, projectDir: historyRollup.FINALIZED_PROJECT_ID, modelId: historyRollup.FINALIZED_MODEL_ID, finalizedAt: 111, v: 2 },
68
+
69
+ // day -1: project A and B, finalized
70
+ { date: d1, projectDir: 'proj-a', modelId: historyRollup.TOTALS_MODEL_ID, promptCount: 3, toolCallCount: 1, toolBreakdown: { Edit: 1 }, errorCount: 1, sessionCount: 1, activeMinutes: 8, v: 2 },
71
+ { date: d1, projectDir: 'proj-a', modelId: 'claude-sonnet-5', promptCount: 0, inputTokens: 500, outputTokens: 100, cacheReadTokens: 0, cacheCreationTokens: 0, v: 2 },
72
+ { date: d1, projectDir: 'proj-b', modelId: historyRollup.TOTALS_MODEL_ID, promptCount: 2, toolCallCount: 0, toolBreakdown: {}, errorCount: 0, sessionCount: 1, activeMinutes: 5, v: 2 },
73
+ { date: d1, projectDir: 'proj-b', modelId: 'claude-opus-5', promptCount: 0, inputTokens: 300, outputTokens: 50, cacheReadTokens: 0, cacheCreationTokens: 0, v: 2 },
74
+ { date: d1, projectDir: historyRollup.FINALIZED_PROJECT_ID, modelId: historyRollup.FINALIZED_MODEL_ID, finalizedAt: 222, v: 2 },
75
+
76
+ // today: provisional, project A only, no finalized marker
77
+ { date: d0, projectDir: 'proj-a', modelId: historyRollup.TOTALS_MODEL_ID, promptCount: 1, toolCallCount: 1, toolBreakdown: { Bash: 1 }, errorCount: 0, sessionCount: 1, activeMinutes: 3, v: 2 },
78
+ { date: d0, projectDir: 'proj-a', modelId: 'claude-sonnet-5', promptCount: 0, inputTokens: 100, outputTokens: 20, cacheReadTokens: 0, cacheCreationTokens: 0, v: 2 },
79
+ ]);
80
+
81
+ return { d2, d1, d0 };
82
+ }
83
+
84
+ test('dashboard response shape: 3-day fixture with rangeDays=30 includes all seeded days, correct totals, provisional today', async () => {
85
+ const { d2, d1, d0 } = await seedThreeDayFixture();
86
+
87
+ const result = await historyDashboard.computeDashboard({ rangeDays: 30 });
88
+
89
+ expect(result.from).toBeTruthy();
90
+ expect(result.to).toBe(d0);
91
+ expect(Array.isArray(result.days)).toBe(true);
92
+
93
+ const dates = result.days.map((d) => d.date);
94
+ expect(dates).toContain(d2);
95
+ expect(dates).toContain(d1);
96
+ expect(dates).toContain(d0);
97
+
98
+ // provisionalDates: only today (no finalized marker was written for it).
99
+ expect(result.provisionalDates).toEqual([d0]);
100
+
101
+ // Totals across all 3 days: promptCount 4+3+2+1 = 10
102
+ expect(result.totals.promptCount).toBe(10);
103
+ expect(result.totals.activeMinutes).toBe(12 + 8 + 5 + 3);
104
+
105
+ // byProjectTotals: proj-a spans all 3 days, proj-b only day -1
106
+ expect(result.byProjectTotals['proj-a'].promptCount).toBe(4 + 3 + 1);
107
+ expect(result.byProjectTotals['proj-b'].promptCount).toBe(2);
108
+
109
+ // byModelTotals: sonnet appears on all 3 days for proj-a + day -1 for... only proj-a uses sonnet
110
+ expect(result.byModelTotals['claude-sonnet-5'].inputTokens).toBe(1000 + 500 + 100);
111
+ expect(result.byModelTotals['claude-sonnet-5'].costUsd).toBeGreaterThan(0);
112
+ expect(result.byModelTotals['claude-opus-5'].inputTokens).toBe(300);
113
+
114
+ // toolsByProject
115
+ expect(result.toolsByProject['proj-a']).toEqual({ Read: 2, Edit: 1, Bash: 1 });
116
+ expect(result.toolsByProject['proj-b']).toEqual({});
117
+
118
+ // Per-day, per-project bucket shape.
119
+ const dayMinus2 = result.days.find((d) => d.date === d2);
120
+ expect(dayMinus2.byProject['proj-a'].promptCount).toBe(4);
121
+ expect(dayMinus2.byProject['proj-a'].byModel['claude-sonnet-5'].costUsd).toBeGreaterThan(0);
122
+
123
+ expect(typeof result.generatedAt).toBe('number');
124
+ });
125
+
126
+ test('dashboard: rangeDays=0 (all time) includes every day with no prior-window bound, prevTotals stays zero', async () => {
127
+ await seedThreeDayFixture();
128
+
129
+ const result = await historyDashboard.computeDashboard({ rangeDays: 0 });
130
+ expect(result.days.length).toBe(3);
131
+ expect(result.prevTotals.promptCount).toBe(0);
132
+ expect(result.prevTotals.estimatedCostUsd).toBe(0);
133
+ });
134
+
135
+ test('dashboard: prevTotals aggregates the prior window distinct from the current window', async () => {
136
+ const d0 = today();
137
+ // Two 1-day windows: rangeDays=1 → current = [today, today], prior = [yesterday, yesterday].
138
+ const yesterday = daysAgo(1);
139
+ await historyRollup.appendRollupDays([
140
+ { date: yesterday, projectDir: 'proj-a', modelId: historyRollup.TOTALS_MODEL_ID, promptCount: 9, sessionCount: 1, activeMinutes: 20, v: 2 },
141
+ { date: yesterday, projectDir: historyRollup.FINALIZED_PROJECT_ID, modelId: historyRollup.FINALIZED_MODEL_ID, finalizedAt: 1, v: 2 },
142
+ { date: d0, projectDir: 'proj-a', modelId: historyRollup.TOTALS_MODEL_ID, promptCount: 2, sessionCount: 1, activeMinutes: 4, v: 2 },
143
+ ]);
144
+
145
+ const result = await historyDashboard.computeDashboard({ rangeDays: 1 });
146
+ expect(result.totals.promptCount).toBe(2);
147
+ expect(result.prevTotals.promptCount).toBe(9);
148
+ });
149
+
150
+ test('zero-scan guarantee: computeDashboard never reads PROJECTS_DIR / transcript files', async () => {
151
+ await seedThreeDayFixture();
152
+
153
+ const fsp = require('node:fs/promises');
154
+ const PROJECTS_DIR = path.join(tmpHome, '.claude', 'projects');
155
+ const readdirSpy = vi.spyOn(fsp, 'readdir');
156
+
157
+ await historyDashboard.computeDashboard({ rangeDays: 30 });
158
+
159
+ const scannedProjectsDir = readdirSpy.mock.calls.some((call) => String(call[0]).startsWith(PROJECTS_DIR));
160
+ expect(scannedProjectsDir).toBe(false);
161
+
162
+ readdirSpy.mockRestore();
163
+ });