From 641ebca5fe5f65f69b657bb6d8fcf98b945a4e04 Mon Sep 17 00:00:00 2001 From: jean-baptiste Date: Sat, 23 May 2026 21:08:52 +0200 Subject: [PATCH 1/4] fix(ipc): use resolveJsonlPath in read-subagent-jsonl and start-subagent-watch --- main.js | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/main.js b/main.js index 19800549..c73e9be7 100644 --- a/main.js +++ b/main.js @@ -291,6 +291,7 @@ sessionCache.init({ }); const { readSessionFile, readFolderFromFilesystem, refreshFolder, populateCacheFromFilesystem, buildProjectsFromCache, notifyRendererProjectsChanged, sendStatus, populateCacheViaWorker } = sessionCache; +const { resolveJsonlPath } = require('./read-session-file'); // --- IPC: browse-folder --- @@ -444,6 +445,26 @@ ipcMain.handle('delete-worktree', (_event, worktreePath) => { }); }); +// --- IPC: worktree-status --- +ipcMain.handle('worktree-status', (_event, worktreePath) => { + return new Promise((resolve) => { + const normalizedPath = worktreePath.replace(/\/$/, ''); + const match = normalizedPath.match(WORKTREE_PATH_RE); + if (!match) { + return resolve({ ok: false, error: 'Path does not match a recognized worktree layout' }); + } + const parentRepo = match[1]; + + execFile('git', ['-C', parentRepo, '-C', normalizedPath, 'status', '--porcelain'], (err, stdout, stderr) => { + if (err) { + return resolve({ ok: false, error: (stderr || err.message || String(err)).trim() }); + } + const dirty = stdout.split('\n').map(l => l.trimEnd()).filter(Boolean); + resolve({ ok: true, dirty, total: dirty.length }); + }); + }); +}); + // --- IPC: get-projects --- ipcMain.handle('open-external', (_event, url) => { log.info('[open-external IPC]', url); @@ -997,7 +1018,7 @@ ipcMain.handle('read-session-jsonl', (_event, sessionId) => { ipcMain.handle('read-subagent-jsonl', (_event, parentSessionId, agentId) => { const row = getCachedSession('sub:' + parentSessionId + ':' + agentId); if (!row) return { error: 'Subagent session not found in cache' }; - const jsonlPath = path.join(PROJECTS_DIR, row.folder, parentSessionId, 'subagents', 'agent-' + agentId + '.jsonl'); + const jsonlPath = resolveJsonlPath(PROJECTS_DIR, row); try { const content = fs.readFileSync(jsonlPath, 'utf-8'); const entries = []; @@ -1027,7 +1048,7 @@ ipcMain.handle('list-subagents', (_event, parentSessionId) => { ipcMain.handle('start-subagent-watch', (_event, parentSessionId, agentId) => { const row = getCachedSession('sub:' + parentSessionId + ':' + agentId); if (!row) return { error: 'Subagent not found in cache' }; - const filePath = path.join(PROJECTS_DIR, row.folder, parentSessionId, 'subagents', 'agent-' + agentId + '.jsonl'); + const filePath = resolveJsonlPath(PROJECTS_DIR, row); const watchId = ++subagentWatcherSeq; let offset = 0; From 310a714f5a0e649bd56df5b2669ddefe685dea90 Mon Sep 17 00:00:00 2001 From: jean-baptiste Date: Sat, 23 May 2026 21:11:31 +0200 Subject: [PATCH 2/4] fix(read-session-file): reject colons in subagentSessionId args + test --- read-session-file.js | 2 ++ test/read-session-file.test.js | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/read-session-file.js b/read-session-file.js index e8cc41df..a0b21f83 100644 --- a/read-session-file.js +++ b/read-session-file.js @@ -6,6 +6,8 @@ const fs = require('fs'); * exactly like top-level sessions (search, archive, rename, etc). */ function subagentSessionId(parentSessionId, agentId) { + if (parentSessionId.includes(':')) throw new TypeError(`parentSessionId must not contain ':': ${parentSessionId}`); + if (agentId.includes(':')) throw new TypeError(`agentId must not contain ':': ${agentId}`); return `sub:${parentSessionId}:${agentId}`; } diff --git a/test/read-session-file.test.js b/test/read-session-file.test.js index 17dbc039..9f562872 100644 --- a/test/read-session-file.test.js +++ b/test/read-session-file.test.js @@ -33,6 +33,12 @@ test('subagentSessionId formats parent and agent ids into the expected colon-del assert.equal(parts[2], agent); }); +test('subagentSessionId throws TypeError when agentId or parentSessionId contains a colon', () => { + assert.throws(() => subagentSessionId('parent:bad', 'agent'), TypeError); + assert.throws(() => subagentSessionId('parent', 'agent:bad'), TypeError); + assert.throws(() => subagentSessionId('par:ent', 'age:nt'), TypeError); +}); + test('resolveJsonlPath returns top-level path when row has no parent/agent', () => { const projectsDir = '/projects'; const row = { folder: 'foo', sessionId: 'session-1' }; From 94567a95109fbf6ab07c6fcc156e6f467c2f40d7 Mon Sep 17 00:00:00 2001 From: jean-baptiste Date: Sat, 23 May 2026 21:13:43 +0200 Subject: [PATCH 3/4] fix(jsonl-viewer): use JSON.stringify key in agentMatchCounters to prevent pipe-char collision --- public/jsonl-viewer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/jsonl-viewer.js b/public/jsonl-viewer.js index 2c1a2bca..90b10d19 100644 --- a/public/jsonl-viewer.js +++ b/public/jsonl-viewer.js @@ -259,7 +259,7 @@ const toolRenderers = { if (!parentSessionId) return el; // Determine which Nth match this block is for fanout deduplication - const counterKey = parentSessionId + '|' + desc + '|' + type; + const counterKey = JSON.stringify([parentSessionId, desc, type]); if (agentMatchCounters[counterKey] === undefined) agentMatchCounters[counterKey] = 0; const matchIndex = agentMatchCounters[counterKey]++; From b7a206a0e6b265100fdda92e28344fd7614f3ce4 Mon Sep 17 00:00:00 2001 From: jean-baptiste Date: Sat, 23 May 2026 21:13:55 +0200 Subject: [PATCH 4/4] =?UTF-8?q?chore:=20verify=20O(1)=20cachedMap=20lookup?= =?UTF-8?q?=20(d58be56)=20already=20on=20main=20=E2=80=94=20no=20action=20?= =?UTF-8?q?needed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit