paneful 0.9.8 → 0.9.10
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 +6 -2
- package/dist/server/install-app.js +56 -1
- package/dist/server/pty-manager.js +25 -1
- package/dist/web/assets/index-BkzI6Ty0.js +515 -0
- package/dist/web/assets/{index-CKAjkmp4.css → index-Bmd9Quiw.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +12 -3
- package/dist/web/assets/index-BTFnHRWw.js +0 -436
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Paneful
|
|
2
2
|
|
|
3
|
-
A terminal multiplexer that runs in your browser
|
|
3
|
+
A fast, GPU-accelerated terminal multiplexer that runs in your browser — or as a native macOS app. Split panes, organize projects, sync with your editor, and detect AI agents and dev servers automatically. One `npm install`, no config.
|
|
4
4
|
|
|
5
5
|
**Website:** [paneful.dev](https://paneful.dev)
|
|
6
6
|
|
|
7
|
-

|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -51,6 +51,10 @@ Requires:
|
|
|
51
51
|
|
|
52
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
53
|
|
|
54
|
+
### GPU Rendering
|
|
55
|
+
|
|
56
|
+
Terminals render via WebGL2 on the GPU by default, which is significantly faster for high-throughput output and multiple panes. Toggle it from the sidebar header (lightning bolt icon) or the command palette. Falls back to the DOM renderer automatically if WebGL2 is unavailable or context is lost. The setting persists across sessions.
|
|
57
|
+
|
|
54
58
|
### Terminal Search
|
|
55
59
|
|
|
56
60
|
Press `Cmd+F` in any focused terminal to search its scrollback. Navigate matches with Enter / Shift+Enter or the up/down buttons. Press Escape to close.
|
|
@@ -158,6 +158,61 @@ func readLockfile() -> (pid: pid_t, port: Int)? {
|
|
|
158
158
|
return (pid, port)
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
class DropWebView: WKWebView {
|
|
162
|
+
override init(frame: CGRect, configuration: WKWebViewConfiguration) {
|
|
163
|
+
super.init(frame: frame, configuration: configuration)
|
|
164
|
+
registerForDraggedTypes([
|
|
165
|
+
.fileURL,
|
|
166
|
+
.URL,
|
|
167
|
+
.string,
|
|
168
|
+
NSPasteboard.PasteboardType("public.url"),
|
|
169
|
+
NSPasteboard.PasteboardType("com.apple.pasteboard.promised-file-url"),
|
|
170
|
+
NSPasteboard.PasteboardType("Apple files promise pasteboard type"),
|
|
171
|
+
])
|
|
172
|
+
}
|
|
173
|
+
required init?(coder: NSCoder) { super.init(coder: coder) }
|
|
174
|
+
|
|
175
|
+
private func extractPaths(from pasteboard: NSPasteboard) -> [String] {
|
|
176
|
+
var paths: [String] = []
|
|
177
|
+
guard let items = pasteboard.pasteboardItems else { return paths }
|
|
178
|
+
for item in items {
|
|
179
|
+
// Try public.url first (file:// URI from VS Code)
|
|
180
|
+
if let urlStr = item.string(forType: NSPasteboard.PasteboardType("public.url")),
|
|
181
|
+
urlStr.hasPrefix("file://"),
|
|
182
|
+
let url = URL(string: urlStr) {
|
|
183
|
+
paths.append(url.path)
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
// Fallback: plain text absolute path
|
|
187
|
+
if let text = item.string(forType: .string), text.hasPrefix("/") {
|
|
188
|
+
paths.append(text.trimmingCharacters(in: .whitespacesAndNewlines))
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return paths
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
|
|
195
|
+
return extractPaths(from: sender.draggingPasteboard).isEmpty
|
|
196
|
+
? super.draggingEntered(sender) : .copy
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
|
|
200
|
+
let paths = extractPaths(from: sender.draggingPasteboard)
|
|
201
|
+
if !paths.isEmpty {
|
|
202
|
+
if let json = try? JSONSerialization.data(withJSONObject: paths),
|
|
203
|
+
let jsonStr = String(data: json, encoding: .utf8) {
|
|
204
|
+
// Convert AppKit coordinates (origin bottom-left) to web coordinates (origin top-left)
|
|
205
|
+
let loc = sender.draggingLocation
|
|
206
|
+
let x = loc.x
|
|
207
|
+
let y = bounds.height - loc.y
|
|
208
|
+
evaluateJavaScript("window.__panefulHandleDrop && window.__panefulHandleDrop(\\(jsonStr), \\(x), \\(y))") { _, _ in }
|
|
209
|
+
}
|
|
210
|
+
return true
|
|
211
|
+
}
|
|
212
|
+
return super.performDragOperation(sender)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
161
216
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
|
162
217
|
var window: NSWindow!
|
|
163
218
|
var webView: WKWebView!
|
|
@@ -244,7 +299,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|
|
244
299
|
let config = WKWebViewConfiguration()
|
|
245
300
|
config.preferences.setValue(true, forKey: "developerExtrasEnabled")
|
|
246
301
|
|
|
247
|
-
webView =
|
|
302
|
+
webView = DropWebView(frame: .zero, configuration: config)
|
|
248
303
|
webView.load(URLRequest(url: URL(string: "http://localhost:\\(self.port)")!))
|
|
249
304
|
|
|
250
305
|
window = NSWindow(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as pty from 'node-pty';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
export class PtyManager {
|
|
4
5
|
sessions = new Map();
|
|
@@ -78,7 +79,9 @@ export class PtyManager {
|
|
|
78
79
|
for (const [terminalId, managed] of this.sessions) {
|
|
79
80
|
try {
|
|
80
81
|
const proc = managed.process.process;
|
|
81
|
-
|
|
82
|
+
const isAgent = proc === 'claude' || proc === 'aider' || proc.startsWith('codex')
|
|
83
|
+
|| (RUNTIME_PROCESSES.has(proc) && this.checkChildCmdline(managed.process.pid));
|
|
84
|
+
if (isAgent) {
|
|
82
85
|
const list = result.get(managed.projectId);
|
|
83
86
|
if (list)
|
|
84
87
|
list.push(terminalId);
|
|
@@ -92,4 +95,25 @@ export class PtyManager {
|
|
|
92
95
|
}
|
|
93
96
|
return result;
|
|
94
97
|
}
|
|
98
|
+
/** Check if any child process of the shell is a known AI agent. */
|
|
99
|
+
checkChildCmdline(shellPid) {
|
|
100
|
+
try {
|
|
101
|
+
const childPids = execSync(`pgrep -P ${shellPid}`, { encoding: 'utf8', timeout: 1000 })
|
|
102
|
+
.trim().split('\n').filter(Boolean);
|
|
103
|
+
if (childPids.length === 0)
|
|
104
|
+
return false;
|
|
105
|
+
for (const pid of childPids) {
|
|
106
|
+
const cmdline = execSync(`ps -o args= -p ${pid}`, { encoding: 'utf8', timeout: 1000 }).trim();
|
|
107
|
+
if (AGENT_CMD_PATTERN.test(cmdline))
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
95
116
|
}
|
|
117
|
+
const RUNTIME_PROCESSES = new Set(['node', 'python', 'python3']);
|
|
118
|
+
// Match agent binary names at the end of a path or as a standalone token
|
|
119
|
+
const AGENT_CMD_PATTERN = /(?:^|\/)(codex|claude|aider)(?:\s|$)/;
|