paneful 0.8.7 → 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.
- package/README.md +12 -8
- package/dist/server/claude-monitor.js +67 -0
- package/dist/server/pty-manager.js +19 -0
- package/dist/server/ws-handler.js +10 -0
- package/dist/web/assets/{index-D9fr7DHS.js → index-CTFgZeZH.js} +56 -56
- package/dist/web/assets/{index-BtMt9GQ0.css → index-CYCoL-NZ.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,17 +26,17 @@ paneful --install-app # Install as a native macOS app
|
|
|
26
26
|
|
|
27
27
|
## Features
|
|
28
28
|
|
|
29
|
-
###
|
|
29
|
+
### Split Pane Layouts
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Five layout presets — columns, rows, main + stack, main + row, and grid. Cycle through them with `Cmd+T` or auto-reorganize with `Cmd+R`.
|
|
32
32
|
|
|
33
|
-
###
|
|
33
|
+
### Project Sidebar
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
Organize terminals by project. Each project gets its own layout, panes, and working directory. Switch instantly from the sidebar. Drag the right edge to resize — width persists across sessions.
|
|
36
36
|
|
|
37
|
-
###
|
|
37
|
+
### Drag & Drop
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
Drag folders from Finder into the sidebar to create projects with the path pre-filled. Drag files into terminal panes to paste their paths as shell-escaped arguments.
|
|
40
40
|
|
|
41
41
|
### Editor Sync
|
|
42
42
|
|
|
@@ -47,9 +47,13 @@ Requires:
|
|
|
47
47
|
1. **Paneful** (native app) or **Terminal** (CLI) added to **System Settings > Privacy & Security > Accessibility**
|
|
48
48
|
2. Editor window title includes the folder name (default in VS Code/Cursor)
|
|
49
49
|
|
|
50
|
-
###
|
|
50
|
+
### Favourites
|
|
51
|
+
|
|
52
|
+
Save a workspace layout as a favourite — name, layout preset, and per-pane commands. Launch any favourite with a click to instantly recreate the setup.
|
|
53
|
+
|
|
54
|
+
### Dev Server Detection
|
|
51
55
|
|
|
52
|
-
|
|
56
|
+
Automatically detects when a dev server starts in a terminal (Vite, Next.js, Angular, etc.). A green dot appears next to the project name in the sidebar while the port is alive, and disappears when it stops. Tracks ports per-terminal so the same port across different projects is handled correctly.
|
|
53
57
|
|
|
54
58
|
### Auto-Reorganize
|
|
55
59
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Terminal had output within this window → Claude is actively working
|
|
2
|
+
const ACTIVE_THRESHOLD = 3_000;
|
|
3
|
+
export class ClaudeMonitor {
|
|
4
|
+
ptyManager;
|
|
5
|
+
onChange;
|
|
6
|
+
lastOutput = new Map(); // terminalId → timestamp
|
|
7
|
+
prevStatuses = {};
|
|
8
|
+
pollTimer = null;
|
|
9
|
+
destroyed = false;
|
|
10
|
+
constructor(ptyManager, onChange) {
|
|
11
|
+
this.ptyManager = ptyManager;
|
|
12
|
+
this.onChange = onChange;
|
|
13
|
+
}
|
|
14
|
+
start() {
|
|
15
|
+
this.pollTimer = setInterval(() => this.poll(), 3000);
|
|
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
|
+
destroy() {
|
|
26
|
+
this.destroyed = true;
|
|
27
|
+
if (this.pollTimer) {
|
|
28
|
+
clearInterval(this.pollTimer);
|
|
29
|
+
this.pollTimer = null;
|
|
30
|
+
}
|
|
31
|
+
this.lastOutput.clear();
|
|
32
|
+
}
|
|
33
|
+
poll() {
|
|
34
|
+
if (this.destroyed)
|
|
35
|
+
return;
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
// Which projects have claude running, and what's the latest output time per project?
|
|
38
|
+
const claudeProjects = this.ptyManager.getClaudeProjects();
|
|
39
|
+
const statuses = {};
|
|
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';
|
|
50
|
+
}
|
|
51
|
+
if (!this.statusesEqual(statuses)) {
|
|
52
|
+
this.prevStatuses = statuses;
|
|
53
|
+
this.onChange(statuses);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
statusesEqual(next) {
|
|
57
|
+
const prevKeys = Object.keys(this.prevStatuses);
|
|
58
|
+
const nextKeys = Object.keys(next);
|
|
59
|
+
if (prevKeys.length !== nextKeys.length)
|
|
60
|
+
return false;
|
|
61
|
+
for (const key of nextKeys) {
|
|
62
|
+
if (this.prevStatuses[key] !== next[key])
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -72,4 +72,23 @@ export class PtyManager {
|
|
|
72
72
|
terminalExists(terminalId) {
|
|
73
73
|
return this.sessions.has(terminalId);
|
|
74
74
|
}
|
|
75
|
+
/** Returns projectId → terminalIds[] for terminals with `claude` as foreground process. */
|
|
76
|
+
getClaudeProjects() {
|
|
77
|
+
const result = new Map();
|
|
78
|
+
for (const [terminalId, managed] of this.sessions) {
|
|
79
|
+
try {
|
|
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]);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// PTY may have been killed
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
75
94
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
2
2
|
import { newProject } from './project-store.js';
|
|
3
3
|
import { PortMonitor } from './port-monitor.js';
|
|
4
|
+
import { ClaudeMonitor } from './claude-monitor.js';
|
|
4
5
|
export class WsHandler {
|
|
5
6
|
wss;
|
|
6
7
|
client = null;
|
|
7
8
|
ptyManager;
|
|
8
9
|
projectStore;
|
|
9
10
|
portMonitor;
|
|
11
|
+
claudeMonitor;
|
|
10
12
|
idleTimer = null;
|
|
11
13
|
onIdle;
|
|
12
14
|
constructor(server, ptyManager, projectStore, options) {
|
|
@@ -16,6 +18,10 @@ export class WsHandler {
|
|
|
16
18
|
this.portMonitor = new PortMonitor((ports) => {
|
|
17
19
|
this.send({ type: 'port:status', ports });
|
|
18
20
|
});
|
|
21
|
+
this.claudeMonitor = new ClaudeMonitor(ptyManager, (statuses) => {
|
|
22
|
+
this.send({ type: 'claude:status', statuses });
|
|
23
|
+
});
|
|
24
|
+
this.claudeMonitor.start();
|
|
19
25
|
this.wss = new WebSocketServer({ noServer: true });
|
|
20
26
|
server.on('upgrade', (req, socket, head) => {
|
|
21
27
|
if (req.url === '/ws') {
|
|
@@ -86,6 +92,7 @@ export class WsHandler {
|
|
|
86
92
|
break;
|
|
87
93
|
case 'pty:kill': {
|
|
88
94
|
this.portMonitor.removeTerminal(msg.terminalId);
|
|
95
|
+
this.claudeMonitor.removeTerminal(msg.terminalId);
|
|
89
96
|
const projectId = this.ptyManager.kill(msg.terminalId);
|
|
90
97
|
if (projectId) {
|
|
91
98
|
this.projectStore.removeTerminal(projectId, msg.terminalId);
|
|
@@ -119,14 +126,17 @@ export class WsHandler {
|
|
|
119
126
|
}
|
|
120
127
|
destroy() {
|
|
121
128
|
this.portMonitor.destroy();
|
|
129
|
+
this.claudeMonitor.destroy();
|
|
122
130
|
}
|
|
123
131
|
handlePtySpawn(terminalId, projectId, cwd) {
|
|
124
132
|
try {
|
|
125
133
|
this.ptyManager.spawn(terminalId, projectId, cwd, (tid, data) => {
|
|
126
134
|
this.send({ type: 'pty:output', terminalId: tid, data });
|
|
127
135
|
this.portMonitor.scanOutput(tid, projectId, data);
|
|
136
|
+
this.claudeMonitor.recordOutput(tid);
|
|
128
137
|
}, (tid, exitCode) => {
|
|
129
138
|
this.portMonitor.removeTerminal(tid);
|
|
139
|
+
this.claudeMonitor.removeTerminal(tid);
|
|
130
140
|
this.send({ type: 'pty:exit', terminalId: tid, exitCode });
|
|
131
141
|
});
|
|
132
142
|
this.projectStore.addTerminal(projectId, terminalId);
|