paneful 0.9.8 → 0.9.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 CHANGED
@@ -1,10 +1,10 @@
1
1
  # Paneful
2
2
 
3
- A terminal multiplexer that runs in your browser. Split panes, organize by project, drag and drop from Finder, sync with your editor all from a single `npm install`.
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
- ![Paneful](screenshot.png)
7
+ ![Paneful](screenshot.gif)
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 = WKWebView(frame: .zero, configuration: config)
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
- if (proc === 'claude' || proc.startsWith('codex')) {
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|$)/;