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.
@@ -14,7 +14,7 @@ export class ClaudeMonitor {
14
14
  resume() {
15
15
  if (this.destroyed || this.pollTimer)
16
16
  return;
17
- this.pollTimer = setInterval(() => this.poll(), 3000);
17
+ this.pollTimer = setInterval(() => this.poll(), 5000);
18
18
  }
19
19
  pause() {
20
20
  if (this.pollTimer) {
@@ -16,7 +16,7 @@ export class GitMonitor {
16
16
  if (this.destroyed || this.pollTimer)
17
17
  return;
18
18
  this.poll();
19
- this.pollTimer = setInterval(() => this.poll(), 10_000);
19
+ this.pollTimer = setInterval(() => this.poll(), 15_000);
20
20
  }
21
21
  pause() {
22
22
  if (this.pollTimer) {
@@ -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 using OS file index (Spotlight on macOS)
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 findBest = (candidates) => {
279
- if (candidates.length === 0)
280
- return null;
281
- if (candidates.length === 1)
282
- return candidates[0];
283
- // Score candidates: exact size + mtime match wins, then size-only, then most recent
284
- let best = null;
285
- for (const candidate of candidates) {
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
- const stat = fs.statSync(candidate);
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 { /* skip inaccessible */ }
292
+ catch { }
301
293
  }
302
- return best?.path ?? candidates[0];
294
+ resolvePathCache.set(cacheKey, { path: resolved, isDirectory, ts: Date.now() });
295
+ res.json({ path: resolved, isDirectory });
303
296
  };
304
- const respond = (resolved) => {
305
- if (!resolved) {
306
- res.json({ path: null });
307
- return;
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 isDirectory = fs.statSync(resolved).isDirectory();
311
- res.json({ path: resolved, isDirectory });
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
- const candidates = stdout.trim().split('\n').filter(Boolean);
324
- respond(findBest(candidates));
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
- execFile('locate', ['-l', '20', '-b', `\\${name}`], (err, stdout) => {
329
- if (err) {
330
- respond(null);
331
- return;
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
- const candidates = stdout.trim().split('\n').filter(Boolean);
334
- respond(findBest(candidates));
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(), 5000);
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.poll();
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() {