paneful 0.9.6 → 0.9.8
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/dist/server/claude-monitor.js +1 -1
- package/dist/server/git-monitor.js +1 -1
- package/dist/server/index.js +147 -50
- package/dist/server/port-monitor.js +13 -2
- package/dist/server/ws-handler.js +9 -0
- package/dist/web/assets/{index-OTBXdJBr.js → index-BTFnHRWw.js} +93 -91
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -268,71 +268,168 @@ async function startServer(devMode, port) {
|
|
|
268
268
|
res.json({ valid: false });
|
|
269
269
|
}
|
|
270
270
|
});
|
|
271
|
-
// Resolve a dropped file's full path
|
|
271
|
+
// Resolve a dropped file's full path via tiered search (stat → find → Spotlight)
|
|
272
|
+
const resolvePathCache = new Map();
|
|
273
|
+
const RESOLVE_CACHE_TTL = 30_000;
|
|
272
274
|
app.post('/api/resolve-path', (req, res) => {
|
|
273
|
-
const { name, size, lastModified } = req.body;
|
|
275
|
+
const { name, size, lastModified, cwd: hintCwd } = req.body;
|
|
274
276
|
if (!name) {
|
|
275
277
|
res.status(400).json({ error: 'name required' });
|
|
276
278
|
return;
|
|
277
279
|
}
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
280
|
+
const cacheKey = `${name}:${size ?? ''}:${lastModified ?? ''}`;
|
|
281
|
+
const cached = resolvePathCache.get(cacheKey);
|
|
282
|
+
if (cached && Date.now() - cached.ts < RESOLVE_CACHE_TTL) {
|
|
283
|
+
res.json({ path: cached.path, isDirectory: cached.isDirectory });
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const respond = (resolved) => {
|
|
287
|
+
let isDirectory = false;
|
|
288
|
+
if (resolved) {
|
|
286
289
|
try {
|
|
287
|
-
|
|
288
|
-
let score = 0;
|
|
289
|
-
if (size && stat.size === size)
|
|
290
|
-
score += 10;
|
|
291
|
-
if (lastModified && Math.abs(stat.mtimeMs - lastModified) < 2000)
|
|
292
|
-
score += 5;
|
|
293
|
-
// Exclude node_modules and hidden dirs to prefer "real" files
|
|
294
|
-
if (!candidate.includes('node_modules') && !candidate.includes('/.'))
|
|
295
|
-
score += 1;
|
|
296
|
-
if (!best || score > best.score || (score === best.score && stat.mtimeMs > best.mtime)) {
|
|
297
|
-
best = { path: candidate, score, mtime: stat.mtimeMs };
|
|
298
|
-
}
|
|
290
|
+
isDirectory = fs.statSync(resolved).isDirectory();
|
|
299
291
|
}
|
|
300
|
-
catch {
|
|
292
|
+
catch { }
|
|
301
293
|
}
|
|
302
|
-
|
|
294
|
+
resolvePathCache.set(cacheKey, { path: resolved, isDirectory, ts: Date.now() });
|
|
295
|
+
res.json({ path: resolved, isDirectory });
|
|
303
296
|
};
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
297
|
+
const homeDir = os.homedir();
|
|
298
|
+
const quickDirs = [
|
|
299
|
+
homeDir,
|
|
300
|
+
path.join(homeDir, 'Desktop'),
|
|
301
|
+
path.join(homeDir, 'Downloads'),
|
|
302
|
+
path.join(homeDir, 'Documents'),
|
|
303
|
+
path.join(homeDir, 'Developer'),
|
|
304
|
+
path.join(homeDir, 'Projects'),
|
|
305
|
+
path.join(homeDir, 'Source'),
|
|
306
|
+
path.join(homeDir, 'src'),
|
|
307
|
+
path.join(homeDir, 'code'),
|
|
308
|
+
path.join(homeDir, 'repos'),
|
|
309
|
+
path.join(homeDir, 'workspace'),
|
|
310
|
+
'/tmp',
|
|
311
|
+
];
|
|
312
|
+
for (const proj of projectStore.list()) {
|
|
313
|
+
const parent = path.dirname(proj.cwd);
|
|
314
|
+
if (!quickDirs.includes(parent))
|
|
315
|
+
quickDirs.push(parent);
|
|
316
|
+
}
|
|
317
|
+
for (const dir of quickDirs) {
|
|
318
|
+
const candidate = path.join(dir, name);
|
|
309
319
|
try {
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
catch {
|
|
314
|
-
res.json({ path: resolved, isDirectory: false });
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
|
-
if (process.platform === 'darwin') {
|
|
318
|
-
execFile('mdfind', [`kMDItemFSName == '${name.replace(/'/g, "\\'")}'`], (err, stdout) => {
|
|
319
|
-
if (err) {
|
|
320
|
-
respond(null);
|
|
320
|
+
const stat = fs.statSync(candidate);
|
|
321
|
+
if (!size || stat.size === size) {
|
|
322
|
+
respond(candidate);
|
|
321
323
|
return;
|
|
322
324
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
325
|
+
}
|
|
326
|
+
catch { }
|
|
327
|
+
}
|
|
328
|
+
const projectCwds = hintCwd ? [hintCwd] : [];
|
|
329
|
+
for (const proj of projectStore.list()) {
|
|
330
|
+
if (proj.cwd !== hintCwd)
|
|
331
|
+
projectCwds.push(proj.cwd);
|
|
332
|
+
}
|
|
333
|
+
const subDirs = ['', 'server', 'src', 'lib', 'app', 'web', 'web/src', 'web/src/lib',
|
|
334
|
+
'web/src/hooks', 'web/src/stores', 'web/src/components', 'test', 'tests', 'scripts'];
|
|
335
|
+
for (const cwd of projectCwds) {
|
|
336
|
+
for (const sub of subDirs) {
|
|
337
|
+
const candidate = sub ? path.join(cwd, sub, name) : path.join(cwd, name);
|
|
338
|
+
try {
|
|
339
|
+
const stat = fs.statSync(candidate);
|
|
340
|
+
if (!size || stat.size === size) {
|
|
341
|
+
respond(candidate);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch { }
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const searchDirs = projectCwds;
|
|
349
|
+
if (searchDirs.length > 0) {
|
|
350
|
+
let found = false;
|
|
351
|
+
let pending = searchDirs.length;
|
|
352
|
+
for (const dir of searchDirs) {
|
|
353
|
+
execFile('find', [
|
|
354
|
+
dir, '-name', name, '-maxdepth', '10',
|
|
355
|
+
'-not', '-path', '*/node_modules/*',
|
|
356
|
+
'-not', '-path', '*/.*/*',
|
|
357
|
+
'-print', '-quit',
|
|
358
|
+
], { timeout: 1000 }, (err, stdout) => {
|
|
359
|
+
pending--;
|
|
360
|
+
if (found)
|
|
361
|
+
return;
|
|
362
|
+
const match = stdout?.trim();
|
|
363
|
+
if (!err && match) {
|
|
364
|
+
try {
|
|
365
|
+
const stat = fs.statSync(match);
|
|
366
|
+
if (!size || stat.size === size) {
|
|
367
|
+
found = true;
|
|
368
|
+
respond(match);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch { }
|
|
373
|
+
}
|
|
374
|
+
if (pending === 0 && !found)
|
|
375
|
+
fallbackSearch();
|
|
376
|
+
});
|
|
377
|
+
}
|
|
326
378
|
}
|
|
327
379
|
else {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
380
|
+
fallbackSearch();
|
|
381
|
+
}
|
|
382
|
+
function fallbackSearch() {
|
|
383
|
+
const findBest = (candidates) => {
|
|
384
|
+
if (candidates.length === 0)
|
|
385
|
+
return null;
|
|
386
|
+
if (candidates.length === 1)
|
|
387
|
+
return candidates[0];
|
|
388
|
+
const capped = candidates.slice(0, 20);
|
|
389
|
+
let best = null;
|
|
390
|
+
for (const candidate of capped) {
|
|
391
|
+
try {
|
|
392
|
+
const stat = fs.statSync(candidate);
|
|
393
|
+
let score = 0;
|
|
394
|
+
if (size && stat.size === size)
|
|
395
|
+
score += 10;
|
|
396
|
+
if (lastModified && Math.abs(stat.mtimeMs - lastModified) < 2000)
|
|
397
|
+
score += 5;
|
|
398
|
+
if (!candidate.includes('node_modules') && !candidate.includes('/.'))
|
|
399
|
+
score += 1;
|
|
400
|
+
if (!best || score > best.score || (score === best.score && stat.mtimeMs > best.mtime)) {
|
|
401
|
+
best = { path: candidate, score, mtime: stat.mtimeMs };
|
|
402
|
+
}
|
|
403
|
+
if (best.score >= 16)
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
catch { }
|
|
332
407
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
408
|
+
return best?.path ?? candidates[0];
|
|
409
|
+
};
|
|
410
|
+
if (process.platform === 'darwin') {
|
|
411
|
+
execFile('mdfind', [
|
|
412
|
+
'-onlyin', homeDir,
|
|
413
|
+
`kMDItemFSName == '${name.replace(/'/g, "\\'")}'`,
|
|
414
|
+
], { timeout: 3000 }, (err, stdout) => {
|
|
415
|
+
if (err) {
|
|
416
|
+
respond(null);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const candidates = stdout.trim().split('\n').filter(Boolean);
|
|
420
|
+
respond(findBest(candidates));
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
execFile('locate', ['-l', '20', '-b', `\\${name}`], { timeout: 3000 }, (err, stdout) => {
|
|
425
|
+
if (err) {
|
|
426
|
+
respond(null);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const candidates = stdout.trim().split('\n').filter(Boolean);
|
|
430
|
+
respond(findBest(candidates));
|
|
431
|
+
});
|
|
432
|
+
}
|
|
336
433
|
}
|
|
337
434
|
});
|
|
338
435
|
// Serve static frontend (production only)
|
|
@@ -7,6 +7,7 @@ export class PortMonitor {
|
|
|
7
7
|
terminals = new Map();
|
|
8
8
|
alivePorts = new Map(); // projectId → alive ports
|
|
9
9
|
pollTimer = null;
|
|
10
|
+
immediatePollTimer = null;
|
|
10
11
|
onChange;
|
|
11
12
|
destroyed = false;
|
|
12
13
|
polling = false;
|
|
@@ -18,7 +19,7 @@ export class PortMonitor {
|
|
|
18
19
|
if (this.destroyed || !this.paused)
|
|
19
20
|
return;
|
|
20
21
|
this.paused = false;
|
|
21
|
-
this.pollTimer = setInterval(() => this.poll(),
|
|
22
|
+
this.pollTimer = setInterval(() => this.poll(), 10_000);
|
|
22
23
|
}
|
|
23
24
|
pause() {
|
|
24
25
|
this.paused = true;
|
|
@@ -26,6 +27,10 @@ export class PortMonitor {
|
|
|
26
27
|
clearInterval(this.pollTimer);
|
|
27
28
|
this.pollTimer = null;
|
|
28
29
|
}
|
|
30
|
+
if (this.immediatePollTimer) {
|
|
31
|
+
clearTimeout(this.immediatePollTimer);
|
|
32
|
+
this.immediatePollTimer = null;
|
|
33
|
+
}
|
|
29
34
|
}
|
|
30
35
|
getPortStatus() {
|
|
31
36
|
const result = {};
|
|
@@ -77,7 +82,9 @@ export class PortMonitor {
|
|
|
77
82
|
}
|
|
78
83
|
}
|
|
79
84
|
if (found && !this.paused) {
|
|
80
|
-
this.
|
|
85
|
+
if (this.immediatePollTimer)
|
|
86
|
+
clearTimeout(this.immediatePollTimer);
|
|
87
|
+
this.immediatePollTimer = setTimeout(() => this.poll(), 500);
|
|
81
88
|
}
|
|
82
89
|
}
|
|
83
90
|
removeTerminal(terminalId) {
|
|
@@ -105,6 +112,10 @@ export class PortMonitor {
|
|
|
105
112
|
clearInterval(this.pollTimer);
|
|
106
113
|
this.pollTimer = null;
|
|
107
114
|
}
|
|
115
|
+
if (this.immediatePollTimer) {
|
|
116
|
+
clearTimeout(this.immediatePollTimer);
|
|
117
|
+
this.immediatePollTimer = null;
|
|
118
|
+
}
|
|
108
119
|
this.terminals.clear();
|
|
109
120
|
this.alivePorts.clear();
|
|
110
121
|
}
|
|
@@ -152,6 +152,15 @@ export class WsHandler {
|
|
|
152
152
|
this.projectStore.remove(msg.projectId);
|
|
153
153
|
break;
|
|
154
154
|
}
|
|
155
|
+
case 'open:url': {
|
|
156
|
+
const url = msg.url;
|
|
157
|
+
if (/^https?:\/\//i.test(url)) {
|
|
158
|
+
import('open').then(({ default: open }) => {
|
|
159
|
+
open(url).catch(() => { });
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
155
164
|
}
|
|
156
165
|
}
|
|
157
166
|
getEditorState() {
|