myrlin-workbook 0.9.28 → 0.9.30
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/package.json +1 -1
- package/scripts/postinstall.js +77 -37
- package/src/web/pty-manager.js +43 -1
- package/src/web/public/app.js +29 -14
- package/src/web/server.js +66 -44
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -1,37 +1,77 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Postinstall script: fix node-pty spawn-helper permissions on macOS/Linux.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Postinstall script: fix node-pty spawn-helper permissions on macOS/Linux.
|
|
4
|
+
*
|
|
5
|
+
* The prebuilt spawn-helper binary in node-pty's package ships without
|
|
6
|
+
* execute permission (mode 644 instead of 755), causing posix_spawnp to
|
|
7
|
+
* fail with "posix_spawnp failed" on first PTY spawn.
|
|
8
|
+
*
|
|
9
|
+
* This is a known node-pty packaging issue. We work around it here by
|
|
10
|
+
* locating node-pty (wherever npm/npx hoisted it), finding all prebuilt
|
|
11
|
+
* spawn-helper binaries across platform dirs, and setting them to 755.
|
|
12
|
+
*
|
|
13
|
+
* See: https://github.com/therealarthur/myrlin-workbook/issues/4
|
|
14
|
+
*/
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
if (process.platform === 'win32') {
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Locate node-pty's package directory using require.resolve.
|
|
26
|
+
* Works regardless of where npm/npx/yarn placed it (hoisted, nested,
|
|
27
|
+
* pnp-virtual, etc.). Returns null if node-pty isn't installed.
|
|
28
|
+
*/
|
|
29
|
+
function findNodePtyDir() {
|
|
30
|
+
try {
|
|
31
|
+
const ptyMain = require.resolve('node-pty', { paths: [path.join(__dirname, '..')] });
|
|
32
|
+
let dir = path.dirname(ptyMain);
|
|
33
|
+
// Walk up until we find package.json with "name": "node-pty"
|
|
34
|
+
for (let i = 0; i < 8; i++) {
|
|
35
|
+
const pkg = path.join(dir, 'package.json');
|
|
36
|
+
if (fs.existsSync(pkg)) {
|
|
37
|
+
try {
|
|
38
|
+
const json = JSON.parse(fs.readFileSync(pkg, 'utf8'));
|
|
39
|
+
if (json && json.name === 'node-pty') return dir;
|
|
40
|
+
} catch (_) {}
|
|
41
|
+
}
|
|
42
|
+
const parent = path.dirname(dir);
|
|
43
|
+
if (parent === dir) break;
|
|
44
|
+
dir = parent;
|
|
45
|
+
}
|
|
46
|
+
} catch (_) {}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const ptyDir = findNodePtyDir();
|
|
51
|
+
if (!ptyDir) {
|
|
52
|
+
// node-pty not installed yet (rare during postinstall); skip silently.
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const prebuildsDir = path.join(ptyDir, 'prebuilds');
|
|
57
|
+
if (!fs.existsSync(prebuildsDir)) {
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const platforms = fs.readdirSync(prebuildsDir, { withFileTypes: true })
|
|
63
|
+
.filter(d => d.isDirectory());
|
|
64
|
+
|
|
65
|
+
for (const platform of platforms) {
|
|
66
|
+
const helper = path.join(prebuildsDir, platform.name, 'spawn-helper');
|
|
67
|
+
if (fs.existsSync(helper)) {
|
|
68
|
+
try {
|
|
69
|
+
fs.chmodSync(helper, 0o755);
|
|
70
|
+
} catch (_) {
|
|
71
|
+
// Non-fatal: runtime fix will catch this on first PTY spawn
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch (_) {
|
|
76
|
+
// Non-fatal
|
|
77
|
+
}
|
package/src/web/pty-manager.js
CHANGED
|
@@ -11,10 +11,52 @@
|
|
|
11
11
|
* - Scrollback is capped at ~100KB total characters
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
const pty = require('node-pty');
|
|
15
14
|
const fs = require('fs');
|
|
16
15
|
const os = require('os');
|
|
17
16
|
const path = require('path');
|
|
17
|
+
|
|
18
|
+
// Ensure node-pty's prebuilt spawn-helper is executable BEFORE requiring node-pty.
|
|
19
|
+
// node-pty's prebuild ships with mode 644 instead of 755, causing posix_spawnp
|
|
20
|
+
// to fail on macOS/Linux. The postinstall script handles this in normal installs
|
|
21
|
+
// but doesn't run with --ignore-scripts or in some npx caches. This runtime
|
|
22
|
+
// fallback covers those cases. See: https://github.com/therealarthur/myrlin-workbook/issues/4
|
|
23
|
+
if (process.platform !== 'win32') {
|
|
24
|
+
try {
|
|
25
|
+
const ptyMain = require.resolve('node-pty');
|
|
26
|
+
let dir = path.dirname(ptyMain);
|
|
27
|
+
for (let i = 0; i < 8; i++) {
|
|
28
|
+
const pkg = path.join(dir, 'package.json');
|
|
29
|
+
if (fs.existsSync(pkg)) {
|
|
30
|
+
try {
|
|
31
|
+
const json = JSON.parse(fs.readFileSync(pkg, 'utf8'));
|
|
32
|
+
if (json && json.name === 'node-pty') break;
|
|
33
|
+
} catch (_) {}
|
|
34
|
+
}
|
|
35
|
+
const parent = path.dirname(dir);
|
|
36
|
+
if (parent === dir) { dir = null; break; }
|
|
37
|
+
dir = parent;
|
|
38
|
+
}
|
|
39
|
+
if (dir) {
|
|
40
|
+
const prebuildsDir = path.join(dir, 'prebuilds');
|
|
41
|
+
if (fs.existsSync(prebuildsDir)) {
|
|
42
|
+
for (const p of fs.readdirSync(prebuildsDir)) {
|
|
43
|
+
const helper = path.join(prebuildsDir, p, 'spawn-helper');
|
|
44
|
+
if (fs.existsSync(helper)) {
|
|
45
|
+
try {
|
|
46
|
+
const stat = fs.statSync(helper);
|
|
47
|
+
// Only chmod if not already executable, avoids unnecessary syscalls
|
|
48
|
+
if ((stat.mode & 0o111) === 0) fs.chmodSync(helper, 0o755);
|
|
49
|
+
} catch (_) {}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch (_) {
|
|
55
|
+
// node-pty not yet resolvable; require() below will throw with a clearer error
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const pty = require('node-pty');
|
|
18
60
|
const { getStore } = require('../state/store');
|
|
19
61
|
|
|
20
62
|
/**
|
package/src/web/public/app.js
CHANGED
|
@@ -3109,10 +3109,16 @@ class CWMApp {
|
|
|
3109
3109
|
this.showToast('Select or create a project first', 'warning');
|
|
3110
3110
|
return;
|
|
3111
3111
|
}
|
|
3112
|
-
//
|
|
3113
|
-
const
|
|
3114
|
-
|
|
3115
|
-
|
|
3112
|
+
// Prefer custom title from Claude session, fall back to folder name + short UUID
|
|
3113
|
+
const customTitle = this.getProjectSessionTitle(sessionName);
|
|
3114
|
+
let friendlyName;
|
|
3115
|
+
if (customTitle) {
|
|
3116
|
+
friendlyName = customTitle;
|
|
3117
|
+
} else {
|
|
3118
|
+
const projectName = projectPath ? projectPath.split('\\').pop() || projectPath.split('/').pop() || sessionName : sessionName;
|
|
3119
|
+
const shortId = sessionName.length > 8 ? sessionName.substring(0, 8) : sessionName;
|
|
3120
|
+
friendlyName = projectName + ' (' + shortId + ')';
|
|
3121
|
+
}
|
|
3116
3122
|
this.api('POST', '/api/sessions', {
|
|
3117
3123
|
name: friendlyName,
|
|
3118
3124
|
workspaceId: this.state.activeWorkspace.id,
|
|
@@ -7230,8 +7236,10 @@ class CWMApp {
|
|
|
7230
7236
|
!p.dirExists ? '<span class="discover-badge discover-badge-missing">missing</span>' : '',
|
|
7231
7237
|
].filter(Boolean).join(' ');
|
|
7232
7238
|
|
|
7233
|
-
const
|
|
7234
|
-
|
|
7239
|
+
const latestSession = p.sessions && p.sessions.length > 0 ? p.sessions[0] : null;
|
|
7240
|
+
const latestSessionId = latestSession ? latestSession.name : '';
|
|
7241
|
+
const latestSessionTitle = latestSession ? (latestSession.title || '') : '';
|
|
7242
|
+
return `<div class="discover-row" data-path="${this.escapeHtml(p.realPath)}" data-name="${this.escapeHtml(name)}" data-session-id="${this.escapeHtml(latestSessionId)}" data-session-title="${this.escapeHtml(latestSessionTitle)}">
|
|
7235
7243
|
<div class="discover-check">
|
|
7236
7244
|
<input type="checkbox" class="discover-cb" ${p.dirExists ? 'checked' : ''} ${!p.dirExists ? 'disabled' : ''}>
|
|
7237
7245
|
</div>
|
|
@@ -7293,7 +7301,7 @@ class CWMApp {
|
|
|
7293
7301
|
const cb = row.querySelector('.discover-cb');
|
|
7294
7302
|
if (cb && cb.checked) {
|
|
7295
7303
|
selected.push({
|
|
7296
|
-
name: row.dataset.name,
|
|
7304
|
+
name: row.dataset.sessionTitle || row.dataset.name,
|
|
7297
7305
|
path: row.dataset.path,
|
|
7298
7306
|
sessionId: row.dataset.sessionId || '',
|
|
7299
7307
|
});
|
|
@@ -9546,9 +9554,9 @@ class CWMApp {
|
|
|
9546
9554
|
const path = p.realPath || '';
|
|
9547
9555
|
// Match against project name, encoded name, or path
|
|
9548
9556
|
if (name.toLowerCase().includes(query) || encoded.toLowerCase().includes(query) || path.toLowerCase().includes(query)) return true;
|
|
9549
|
-
// Match against any session ID
|
|
9557
|
+
// Match against any session ID, title, or Claude custom-title within this project
|
|
9550
9558
|
const allSessions = p.sessions || [];
|
|
9551
|
-
return allSessions.some(s => (s.name || '').toLowerCase().includes(query));
|
|
9559
|
+
return allSessions.some(s => (s.name || '').toLowerCase().includes(query) || (s.title || '').toLowerCase().includes(query));
|
|
9552
9560
|
});
|
|
9553
9561
|
}
|
|
9554
9562
|
|
|
@@ -9579,8 +9587,9 @@ class CWMApp {
|
|
|
9579
9587
|
if (!projectMatches) {
|
|
9580
9588
|
sessions = sessions.filter(s => {
|
|
9581
9589
|
const sName = (s.name || '').toLowerCase();
|
|
9590
|
+
const sClaudeTitle = (s.title || '').toLowerCase();
|
|
9582
9591
|
const sTitle = (this.getProjectSessionTitle(s.name) || '').toLowerCase();
|
|
9583
|
-
return sName.includes(query) || sTitle.includes(query);
|
|
9592
|
+
return sName.includes(query) || sClaudeTitle.includes(query) || sTitle.includes(query);
|
|
9584
9593
|
});
|
|
9585
9594
|
}
|
|
9586
9595
|
}
|
|
@@ -9588,13 +9597,19 @@ class CWMApp {
|
|
|
9588
9597
|
// Build session sub-items
|
|
9589
9598
|
const sessionItems = sessions.map(s => {
|
|
9590
9599
|
const sessName = s.name || 'unnamed';
|
|
9600
|
+
const claudeTitle = s.title || null;
|
|
9601
|
+
if (claudeTitle && !this.getProjectSessionTitle(sessName)) {
|
|
9602
|
+
const titles = JSON.parse(localStorage.getItem('cwm_projectSessionTitles') || '{}');
|
|
9603
|
+
titles[sessName] = claudeTitle;
|
|
9604
|
+
localStorage.setItem('cwm_projectSessionTitles', JSON.stringify(titles));
|
|
9605
|
+
}
|
|
9591
9606
|
const storedTitle = this.getProjectSessionTitle(sessName);
|
|
9592
|
-
const displayName = storedTitle || (sessName.length > 24 ? sessName.substring(0, 24) + '...' : sessName);
|
|
9607
|
+
const displayName = storedTitle || claudeTitle || (sessName.length > 24 ? sessName.substring(0, 24) + '...' : sessName);
|
|
9593
9608
|
const sessSize = s.size ? this.formatSize(s.size) : '';
|
|
9594
9609
|
const sessTime = s.modified ? this.relativeTime(s.modified) : '';
|
|
9595
|
-
|
|
9596
|
-
const tooltip =
|
|
9597
|
-
? `${
|
|
9610
|
+
const effectiveTitle = storedTitle || claudeTitle;
|
|
9611
|
+
const tooltip = effectiveTitle
|
|
9612
|
+
? `${effectiveTitle}\n\nSession: ${sessName}`
|
|
9598
9613
|
: sessName;
|
|
9599
9614
|
return `<div class="project-session-item" draggable="true" data-session-name="${this.escapeHtml(sessName)}" data-project-path="${this.escapeHtml(p.realPath || '')}" data-project-encoded="${this.escapeHtml(encoded)}" title="${this.escapeHtml(tooltip)}">
|
|
9600
9615
|
<span class="project-session-name">${this.escapeHtml(displayName)}</span>
|
package/src/web/server.js
CHANGED
|
@@ -1646,6 +1646,13 @@ app.get('/api/discover', requireAuth, (req, res) => {
|
|
|
1646
1646
|
// skip if can't read
|
|
1647
1647
|
}
|
|
1648
1648
|
|
|
1649
|
+
// Extract the last custom-title from each JSONL file
|
|
1650
|
+
const sessionTitles = {};
|
|
1651
|
+
for (const sf of sessionFiles) {
|
|
1652
|
+
const title = extractCustomTitle(path.join(projectDir, sf.name + '.jsonl'));
|
|
1653
|
+
if (title) sessionTitles[sf.name] = title;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1649
1656
|
// Check for CLAUDE.md
|
|
1650
1657
|
let hasClaudeMd = false;
|
|
1651
1658
|
try {
|
|
@@ -1672,7 +1679,7 @@ app.get('/api/discover', requireAuth, (req, res) => {
|
|
|
1672
1679
|
sessionCount: sessionFiles.length,
|
|
1673
1680
|
totalSize,
|
|
1674
1681
|
lastActive: sessionFiles.length > 0 ? sessionFiles[0].modified : null,
|
|
1675
|
-
sessions: sessionFiles.map(s => ({ name: s.name, modified: s.modified, size: s.size })),
|
|
1682
|
+
sessions: sessionFiles.map(s => ({ name: s.name, modified: s.modified, size: s.size, title: sessionTitles[s.name] || null })),
|
|
1676
1683
|
});
|
|
1677
1684
|
}
|
|
1678
1685
|
|
|
@@ -1801,48 +1808,34 @@ function greedyFsWalk(root, tokens) {
|
|
|
1801
1808
|
*/
|
|
1802
1809
|
function getOriginalPathFromJsonl(projectDir) {
|
|
1803
1810
|
try {
|
|
1804
|
-
const
|
|
1805
|
-
// Find the first .jsonl file (skip subdirectories)
|
|
1806
|
-
const jsonlFile = files.find(f => {
|
|
1811
|
+
const jsonlFiles = fs.readdirSync(projectDir).filter(f => {
|
|
1807
1812
|
if (!f.endsWith('.jsonl')) return false;
|
|
1808
|
-
try {
|
|
1809
|
-
return !fs.statSync(path.join(projectDir, f)).isDirectory();
|
|
1810
|
-
} catch (_) {
|
|
1811
|
-
return false;
|
|
1812
|
-
}
|
|
1813
|
+
try { return !fs.statSync(path.join(projectDir, f)).isDirectory(); } catch (_) { return false; }
|
|
1813
1814
|
});
|
|
1814
|
-
if (!jsonlFile) return null;
|
|
1815
|
-
|
|
1816
|
-
const jsonlPath = path.join(projectDir, jsonlFile);
|
|
1817
1815
|
|
|
1818
|
-
//
|
|
1819
|
-
const
|
|
1820
|
-
const buffer = Buffer.alloc(4096);
|
|
1821
|
-
let content;
|
|
1822
|
-
try {
|
|
1823
|
-
const bytesRead = fs.readSync(fd, buffer, 0, 4096, 0);
|
|
1824
|
-
content = buffer.toString('utf-8', 0, bytesRead);
|
|
1825
|
-
} finally {
|
|
1826
|
-
fs.closeSync(fd);
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
// Parse first 3 lines to find cwd field
|
|
1830
|
-
const lines = content.split('\n').slice(0, 3);
|
|
1831
|
-
for (const line of lines) {
|
|
1832
|
-
if (!line.trim()) continue;
|
|
1816
|
+
// Try each file until one yields a cwd — stub sessions may lack it
|
|
1817
|
+
for (const jsonlFile of jsonlFiles) {
|
|
1833
1818
|
try {
|
|
1834
|
-
const
|
|
1835
|
-
|
|
1836
|
-
|
|
1819
|
+
const fd = fs.openSync(path.join(projectDir, jsonlFile), 'r');
|
|
1820
|
+
let content;
|
|
1821
|
+
try {
|
|
1822
|
+
const buffer = Buffer.alloc(16384);
|
|
1823
|
+
const bytesRead = fs.readSync(fd, buffer, 0, 16384, 0);
|
|
1824
|
+
content = buffer.toString('utf-8', 0, bytesRead);
|
|
1825
|
+
} finally {
|
|
1826
|
+
fs.closeSync(fd);
|
|
1837
1827
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1828
|
+
|
|
1829
|
+
for (const line of content.split('\n')) {
|
|
1830
|
+
if (!line.includes('"cwd"')) continue;
|
|
1831
|
+
try {
|
|
1832
|
+
const record = JSON.parse(line);
|
|
1833
|
+
if (record.cwd && typeof record.cwd === 'string') return record.cwd;
|
|
1834
|
+
} catch (_) { continue; }
|
|
1835
|
+
}
|
|
1836
|
+
} catch (_) { continue; }
|
|
1842
1837
|
}
|
|
1843
|
-
} catch (_) {
|
|
1844
|
-
// Silently fail and return null
|
|
1845
|
-
}
|
|
1838
|
+
} catch (_) {}
|
|
1846
1839
|
return null;
|
|
1847
1840
|
}
|
|
1848
1841
|
|
|
@@ -2524,7 +2517,7 @@ app.post('/api/search-conversations', requireAuth, async (req, res) => {
|
|
|
2524
2517
|
*/
|
|
2525
2518
|
app.get('/api/keys/anthropic', requireAuth, (req, res) => {
|
|
2526
2519
|
const store = getStore();
|
|
2527
|
-
const key = (store.
|
|
2520
|
+
const key = (store.state.settings || {}).anthropicApiKey || '';
|
|
2528
2521
|
if (!key) return res.json({ configured: false, masked: null });
|
|
2529
2522
|
const masked = '...' + key.slice(-8);
|
|
2530
2523
|
return res.json({ configured: true, masked });
|
|
@@ -2560,7 +2553,7 @@ app.post('/api/ai/punctuate', requireAuth, async (req, res) => {
|
|
|
2560
2553
|
}
|
|
2561
2554
|
|
|
2562
2555
|
const store = getStore();
|
|
2563
|
-
const apiKey = (store.
|
|
2556
|
+
const apiKey = (store.state.settings || {}).anthropicApiKey || '';
|
|
2564
2557
|
if (!apiKey) {
|
|
2565
2558
|
return res.status(400).json({ error: 'No Anthropic API key configured' });
|
|
2566
2559
|
}
|
|
@@ -2612,7 +2605,7 @@ app.post('/api/ai/find-session', requireAuth, async (req, res) => {
|
|
|
2612
2605
|
}
|
|
2613
2606
|
|
|
2614
2607
|
const store = getStore();
|
|
2615
|
-
const apiKey = (store.
|
|
2608
|
+
const apiKey = (store.state.settings || {}).anthropicApiKey || '';
|
|
2616
2609
|
|
|
2617
2610
|
// Gather all metadata
|
|
2618
2611
|
const workspaces = store.getAllWorkspacesList();
|
|
@@ -2631,6 +2624,7 @@ app.post('/api/ai/find-session', requireAuth, async (req, res) => {
|
|
|
2631
2624
|
const name = getProjectDisplayName(d.name, realPath);
|
|
2632
2625
|
let sessionCount = 0;
|
|
2633
2626
|
let lastActive = null;
|
|
2627
|
+
const sessionTitles = [];
|
|
2634
2628
|
try {
|
|
2635
2629
|
const files = fs.readdirSync(projectDir).filter(f => f.endsWith('.jsonl'));
|
|
2636
2630
|
sessionCount = files.length;
|
|
@@ -2639,9 +2633,11 @@ app.post('/api/ai/find-session', requireAuth, async (req, res) => {
|
|
|
2639
2633
|
const stat = fs.statSync(path.join(projectDir, f));
|
|
2640
2634
|
if (!lastActive || stat.mtime > new Date(lastActive)) lastActive = stat.mtime;
|
|
2641
2635
|
} catch (_) {}
|
|
2636
|
+
const title = extractCustomTitle(path.join(projectDir, f));
|
|
2637
|
+
if (title) sessionTitles.push(title);
|
|
2642
2638
|
}
|
|
2643
2639
|
} catch (_) {}
|
|
2644
|
-
return { encodedName: d.name, name, path: realPath, sessionCount, lastActive };
|
|
2640
|
+
return { encodedName: d.name, name, path: realPath, sessionCount, lastActive, sessionTitles };
|
|
2645
2641
|
});
|
|
2646
2642
|
} catch (_) {}
|
|
2647
2643
|
}
|
|
@@ -2658,7 +2654,8 @@ app.post('/api/ai/find-session', requireAuth, async (req, res) => {
|
|
|
2658
2654
|
})),
|
|
2659
2655
|
discoveredProjects: discoveredProjects.map(p => ({
|
|
2660
2656
|
encodedName: p.encodedName, name: p.name, path: p.path,
|
|
2661
|
-
sessionCount: p.sessionCount, lastActive: p.lastActive
|
|
2657
|
+
sessionCount: p.sessionCount, lastActive: p.lastActive,
|
|
2658
|
+
sessionTitles: p.sessionTitles
|
|
2662
2659
|
}))
|
|
2663
2660
|
};
|
|
2664
2661
|
|
|
@@ -2722,9 +2719,9 @@ app.post('/api/ai/find-session', requireAuth, async (req, res) => {
|
|
|
2722
2719
|
if (score > 0.2) scored.push({ type: 'session', id: s.id, confidence: score, summary: 'Matched by name, topic, or path' });
|
|
2723
2720
|
}
|
|
2724
2721
|
for (const p of discoveredProjects) {
|
|
2725
|
-
const text = `${p.name} ${p.path}`.toLowerCase();
|
|
2722
|
+
const text = `${p.name} ${p.path} ${(p.sessionTitles || []).join(' ')}`.toLowerCase();
|
|
2726
2723
|
const score = terms.filter(t => text.includes(t)).length / terms.length;
|
|
2727
|
-
if (score > 0.2) scored.push({ type: 'project', id: p.encodedName, confidence: score, summary: 'Matched by project name or
|
|
2724
|
+
if (score > 0.2) scored.push({ type: 'project', id: p.encodedName, confidence: score, summary: 'Matched by project name, path, or session title' });
|
|
2728
2725
|
}
|
|
2729
2726
|
|
|
2730
2727
|
scored.sort((a, b) => b.confidence - a.confidence);
|
|
@@ -7243,6 +7240,30 @@ function getSearchableFiles() {
|
|
|
7243
7240
|
* @param {string} sessionId - Fallback UUID
|
|
7244
7241
|
* @returns {string} A human-readable session name
|
|
7245
7242
|
*/
|
|
7243
|
+
function extractCustomTitle(jsonlPath) {
|
|
7244
|
+
try {
|
|
7245
|
+
const fd = fs.openSync(jsonlPath, 'r');
|
|
7246
|
+
try {
|
|
7247
|
+
const size = fs.fstatSync(fd).size;
|
|
7248
|
+
const tailSize = Math.min(131072, size);
|
|
7249
|
+
const buf = Buffer.alloc(tailSize);
|
|
7250
|
+
fs.readSync(fd, buf, 0, tailSize, size - tailSize);
|
|
7251
|
+
const lines = buf.toString('utf8').split('\n');
|
|
7252
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
7253
|
+
if (lines[i].includes('"custom-title"')) {
|
|
7254
|
+
try {
|
|
7255
|
+
const obj = JSON.parse(lines[i]);
|
|
7256
|
+
if (obj.customTitle) return obj.customTitle;
|
|
7257
|
+
} catch (_) {}
|
|
7258
|
+
}
|
|
7259
|
+
}
|
|
7260
|
+
} finally {
|
|
7261
|
+
fs.closeSync(fd);
|
|
7262
|
+
}
|
|
7263
|
+
} catch (_) {}
|
|
7264
|
+
return null;
|
|
7265
|
+
}
|
|
7266
|
+
|
|
7246
7267
|
function extractSessionName(filePath, sessionId) {
|
|
7247
7268
|
try {
|
|
7248
7269
|
// Read just the first 10KB to find the first meaningful message
|
|
@@ -8054,4 +8075,5 @@ module.exports = {
|
|
|
8054
8075
|
startServer,
|
|
8055
8076
|
getPtyManager,
|
|
8056
8077
|
structuredError,
|
|
8078
|
+
extractCustomTitle,
|
|
8057
8079
|
};
|