paneful 0.8.8 → 0.8.9
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.
|
@@ -1,97 +1,58 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
// If the latest JSONL mtime is < 5s ago, Claude is actively working.
|
|
5
|
-
// Otherwise Claude is open but idle (waiting for user input).
|
|
6
|
-
const ACTIVE_THRESHOLD = 5_000;
|
|
1
|
+
// Terminal had output within this window → Claude is actively working
|
|
2
|
+
const ACTIVE_THRESHOLD = 3_000;
|
|
7
3
|
export class ClaudeMonitor {
|
|
8
|
-
claudeDir;
|
|
9
4
|
ptyManager;
|
|
10
|
-
projectStore;
|
|
11
5
|
onChange;
|
|
6
|
+
lastOutput = new Map(); // terminalId → timestamp
|
|
12
7
|
prevStatuses = {};
|
|
13
|
-
cachedLatestFile = new Map(); // cwd → latest .jsonl path
|
|
14
8
|
pollTimer = null;
|
|
15
9
|
destroyed = false;
|
|
16
|
-
constructor(ptyManager,
|
|
10
|
+
constructor(ptyManager, onChange) {
|
|
17
11
|
this.ptyManager = ptyManager;
|
|
18
|
-
this.projectStore = projectStore;
|
|
19
12
|
this.onChange = onChange;
|
|
20
|
-
this.claudeDir = path.join(os.homedir(), '.claude', 'projects');
|
|
21
13
|
}
|
|
22
14
|
start() {
|
|
23
15
|
this.pollTimer = setInterval(() => this.poll(), 3000);
|
|
24
16
|
}
|
|
17
|
+
/** Call from the PTY output path to record activity. */
|
|
18
|
+
recordOutput(terminalId) {
|
|
19
|
+
this.lastOutput.set(terminalId, Date.now());
|
|
20
|
+
}
|
|
21
|
+
/** Clean up when a terminal is removed. */
|
|
22
|
+
removeTerminal(terminalId) {
|
|
23
|
+
this.lastOutput.delete(terminalId);
|
|
24
|
+
}
|
|
25
25
|
destroy() {
|
|
26
26
|
this.destroyed = true;
|
|
27
27
|
if (this.pollTimer) {
|
|
28
28
|
clearInterval(this.pollTimer);
|
|
29
29
|
this.pollTimer = null;
|
|
30
30
|
}
|
|
31
|
+
this.lastOutput.clear();
|
|
31
32
|
}
|
|
32
33
|
poll() {
|
|
33
34
|
if (this.destroyed)
|
|
34
35
|
return;
|
|
35
|
-
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
// Which projects have claude running, and what's the latest output time per project?
|
|
36
38
|
const claudeProjects = this.ptyManager.getClaudeProjects();
|
|
37
|
-
// Step 2: For each, determine active vs idle by checking JSONL mtime
|
|
38
39
|
const statuses = {};
|
|
39
|
-
for (const projectId of claudeProjects) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
for (const [projectId, terminalIds] of claudeProjects) {
|
|
41
|
+
let latestOutput = 0;
|
|
42
|
+
for (const tid of terminalIds) {
|
|
43
|
+
const ts = this.lastOutput.get(tid) ?? 0;
|
|
44
|
+
if (ts > latestOutput)
|
|
45
|
+
latestOutput = ts;
|
|
46
|
+
}
|
|
47
|
+
statuses[projectId] = (latestOutput > 0 && (now - latestOutput) < ACTIVE_THRESHOLD)
|
|
48
|
+
? 'active'
|
|
49
|
+
: 'idle';
|
|
45
50
|
}
|
|
46
|
-
// Step 3: Only notify if something changed
|
|
47
51
|
if (!this.statusesEqual(statuses)) {
|
|
48
52
|
this.prevStatuses = statuses;
|
|
49
53
|
this.onChange(statuses);
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
|
-
/** Returns true if the project's latest JSONL was modified within ACTIVE_THRESHOLD. */
|
|
53
|
-
checkMtime(cwd) {
|
|
54
|
-
// Fast path: stat only the cached latest file
|
|
55
|
-
const cached = this.cachedLatestFile.get(cwd);
|
|
56
|
-
if (cached) {
|
|
57
|
-
try {
|
|
58
|
-
const stat = fs.statSync(cached);
|
|
59
|
-
if ((Date.now() - stat.mtimeMs) < ACTIVE_THRESHOLD)
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
// File gone — fall through to full scan
|
|
64
|
-
this.cachedLatestFile.delete(cwd);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
// Full scan: find the latest .jsonl (runs once on first check, then only
|
|
68
|
-
// when the cached file is stale — i.e. Claude started a new session)
|
|
69
|
-
const folder = path.join(this.claudeDir, cwd.replace(/\//g, '-'));
|
|
70
|
-
try {
|
|
71
|
-
const files = fs.readdirSync(folder).filter((f) => f.endsWith('.jsonl'));
|
|
72
|
-
let maxMtime = 0;
|
|
73
|
-
let maxFile = '';
|
|
74
|
-
for (const file of files) {
|
|
75
|
-
try {
|
|
76
|
-
const filePath = path.join(folder, file);
|
|
77
|
-
const stat = fs.statSync(filePath);
|
|
78
|
-
if (stat.mtimeMs > maxMtime) {
|
|
79
|
-
maxMtime = stat.mtimeMs;
|
|
80
|
-
maxFile = filePath;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
// skip
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (maxFile)
|
|
88
|
-
this.cachedLatestFile.set(cwd, maxFile);
|
|
89
|
-
return maxMtime > 0 && (Date.now() - maxMtime) < ACTIVE_THRESHOLD;
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
56
|
statusesEqual(next) {
|
|
96
57
|
const prevKeys = Object.keys(this.prevStatuses);
|
|
97
58
|
const nextKeys = Object.keys(next);
|
|
@@ -72,14 +72,17 @@ export class PtyManager {
|
|
|
72
72
|
terminalExists(terminalId) {
|
|
73
73
|
return this.sessions.has(terminalId);
|
|
74
74
|
}
|
|
75
|
-
/** Returns
|
|
75
|
+
/** Returns projectId → terminalIds[] for terminals with `claude` as foreground process. */
|
|
76
76
|
getClaudeProjects() {
|
|
77
|
-
const result = new
|
|
78
|
-
for (const managed of this.sessions
|
|
77
|
+
const result = new Map();
|
|
78
|
+
for (const [terminalId, managed] of this.sessions) {
|
|
79
79
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
if (managed.process.process === 'claude') {
|
|
81
|
+
const list = result.get(managed.projectId);
|
|
82
|
+
if (list)
|
|
83
|
+
list.push(terminalId);
|
|
84
|
+
else
|
|
85
|
+
result.set(managed.projectId, [terminalId]);
|
|
83
86
|
}
|
|
84
87
|
}
|
|
85
88
|
catch {
|
|
@@ -18,7 +18,7 @@ export class WsHandler {
|
|
|
18
18
|
this.portMonitor = new PortMonitor((ports) => {
|
|
19
19
|
this.send({ type: 'port:status', ports });
|
|
20
20
|
});
|
|
21
|
-
this.claudeMonitor = new ClaudeMonitor(ptyManager,
|
|
21
|
+
this.claudeMonitor = new ClaudeMonitor(ptyManager, (statuses) => {
|
|
22
22
|
this.send({ type: 'claude:status', statuses });
|
|
23
23
|
});
|
|
24
24
|
this.claudeMonitor.start();
|
|
@@ -92,6 +92,7 @@ export class WsHandler {
|
|
|
92
92
|
break;
|
|
93
93
|
case 'pty:kill': {
|
|
94
94
|
this.portMonitor.removeTerminal(msg.terminalId);
|
|
95
|
+
this.claudeMonitor.removeTerminal(msg.terminalId);
|
|
95
96
|
const projectId = this.ptyManager.kill(msg.terminalId);
|
|
96
97
|
if (projectId) {
|
|
97
98
|
this.projectStore.removeTerminal(projectId, msg.terminalId);
|
|
@@ -132,8 +133,10 @@ export class WsHandler {
|
|
|
132
133
|
this.ptyManager.spawn(terminalId, projectId, cwd, (tid, data) => {
|
|
133
134
|
this.send({ type: 'pty:output', terminalId: tid, data });
|
|
134
135
|
this.portMonitor.scanOutput(tid, projectId, data);
|
|
136
|
+
this.claudeMonitor.recordOutput(tid);
|
|
135
137
|
}, (tid, exitCode) => {
|
|
136
138
|
this.portMonitor.removeTerminal(tid);
|
|
139
|
+
this.claudeMonitor.removeTerminal(tid);
|
|
137
140
|
this.send({ type: 'pty:exit', terminalId: tid, exitCode });
|
|
138
141
|
});
|
|
139
142
|
this.projectStore.addTerminal(projectId, terminalId);
|