Skip to content

Commit b717efb

Browse files
authored
feat: add agent connection diagnostics, JSONL parser resilience (#183)
* feat: add agent connection diagnostics, JSONL parser resilience, and path encoding fuzzy matching * fix: simplify file watching to single poll, drop unreliable fs.watch/fs.watchFile * docs: add rebase check to PR template * docs: add Debug View guidance to bug report template * fix: patch VS Code product.json to bypass Updating in progress in e2e tests
1 parent 28f7f0a commit b717efb

12 files changed

Lines changed: 336 additions & 51 deletions

File tree

.github/ISSUE_TEMPLATE/bug_report.yml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,23 @@ body:
4444
id: screenshots
4545
attributes:
4646
label: "Screenshots / GIFs"
47-
description: "If applicable, add screenshots or screen recordings."
47+
description: "If applicable, add screenshots or screen recordings. For agent tracking issues (stuck on idle, not spawning), include a screenshot of the **Debug View**: in the Pixel Agents panel, click the gear icon (Settings) and toggle Debug View. This shows JSONL connection status, lines parsed, and file paths. **Note:** paths may contain your username -- redact anything sensitive before posting."
4848

4949
- type: input
5050
id: vscode-version
5151
attributes:
5252
label: "VS Code version"
5353
description: "Run 'Help > About' to find your version."
54-
placeholder: "e.g., 1.109.0"
54+
placeholder: "e.g., 1.112.0"
55+
validations:
56+
required: true
57+
58+
- type: input
59+
id: claude-code-version
60+
attributes:
61+
label: "Claude Code CLI version"
62+
description: "Run `claude --version` in your terminal."
63+
placeholder: "e.g., 2.1.78"
5564
validations:
5665
required: true
5766

@@ -63,12 +72,13 @@ body:
6372
- Windows
6473
- macOS
6574
- Linux
75+
- WSL2
6676
validations:
6777
required: true
6878

6979
- type: textarea
7080
id: logs
7181
attributes:
7282
label: "Logs"
73-
description: "Open Developer Tools (Help > Toggle Developer Tools) and paste any relevant console output."
83+
description: "If running from source (F5), open **View > Debug Console** and paste any lines containing `[Pixel Agents]`. Otherwise, open **Help > Toggle Developer Tools > Console** and paste relevant output. **Note:** logs may contain file paths with your username -- redact anything sensitive before posting."
7484
render: shell

.github/pull_request_template.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
- [ ] Refactor / code cleanup
99
- [ ] Documentation
1010
- [ ] CI / build
11-
- [ ] Other: ___
11+
- [ ] Other: ...
1212

1313
## Related issues
1414
<!-- Link related issues: Closes #123, Fixes #456 -->
@@ -17,9 +17,7 @@
1717
<!-- Required for UI changes. Delete this section if not applicable. -->
1818

1919
## Test plan
20-
<!-- Check what applies, add your own test steps -->
21-
- [ ] PR targets `main` branch
22-
- [ ] `npm run build` passes locally
20+
<!-- Add your own test steps -->
21+
- [ ] Rebased on latest `main`
2322
- [ ] Tested in Extension Development Host (F5)
24-
- [ ] No inline constants (all in `src/constants.ts` or `webview-ui/src/constants.ts`)
25-
- [ ] UI follows pixel art style (sharp corners, solid borders, pixel font)
23+
- [ ] ...

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,13 @@ The webview runs a lightweight game loop with canvas rendering, BFS pathfinding,
119119
- **Heuristic-based status detection** — Claude Code's JSONL transcript format does not provide clear signals for when an agent is waiting for user input or when it has finished its turn. The current detection is based on heuristics (idle timers, turn-duration events) and often misfires — agents may briefly show the wrong status or miss transitions.
120120
- **Linux/macOS tip** — if you launch VS Code without a folder open (e.g. bare `code` command), agents will start in your home directory. This is fully supported; just be aware your Claude sessions will be tracked under `~/.claude/projects/` using your home directory as the project root.
121121

122+
## Troubleshooting
123+
124+
If your agent appears stuck on idle or doesn't spawn:
125+
126+
1. **Debug View** — In the Pixel Agents panel, click the gear icon (Settings), then toggle **Debug View**. This shows connection diagnostics per agent: JSONL file status, lines parsed, last data timestamp, and file path. If you see "JSONL not found", the extension can't locate the session file.
127+
2. **Debug Console** — If you're running from source (Extension Development Host via F5), open VS Code's **View > Debug Console**. Search for `[Pixel Agents]` to see detailed logs: project directory resolution, JSONL polling status, path encoding mismatches, and unrecognized JSONL record types.
128+
122129
## Where This Is Going
123130

124131
The long-term vision is an interface where managing AI agents feels like playing the Sims, but the results are real things built.

e2e/global-setup.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,63 @@ import path from 'path';
55
export const VSCODE_CACHE_DIR = path.join(__dirname, '../.vscode-test');
66
export const VSCODE_PATH_FILE = path.join(VSCODE_CACHE_DIR, 'vscode-executable.txt');
77

8+
/**
9+
* On Windows, VS Code checks for an InnoSetup mutex (`win32MutexName + "-updating"`)
10+
* at startup. If the host machine's VS Code installer is running (e.g. a pending
11+
* "Restart to Update"), the mutex is held and ALL VS Code instances — including our
12+
* test archive — refuse to start with "Code is currently being updated".
13+
*
14+
* The check in main.js is:
15+
* if (!(isWindows && product.win32MutexName && product.win32VersionedUpdate)) return false;
16+
*
17+
* Removing `win32VersionedUpdate` from product.json makes the check short-circuit,
18+
* so the test instance launches regardless of installer state. This is safe because
19+
* the test archive is not managed by InnoSetup and never needs update coordination.
20+
*/
21+
function patchProductJsonForWindows(vscodePath: string): void {
22+
if (process.platform !== 'win32') return;
23+
24+
// vscodePath points to Code.exe — product.json is in the resources/app dir
25+
const vscodeDir = path.dirname(vscodePath);
26+
const candidates = fs
27+
.readdirSync(vscodeDir)
28+
.filter((d) => {
29+
try {
30+
return fs.statSync(path.join(vscodeDir, d)).isDirectory();
31+
} catch {
32+
return false;
33+
}
34+
})
35+
.map((d) => path.join(vscodeDir, d, 'resources', 'app', 'product.json'))
36+
.filter((p) => fs.existsSync(p));
37+
38+
for (const productJsonPath of candidates) {
39+
try {
40+
const product = JSON.parse(fs.readFileSync(productJsonPath, 'utf8'));
41+
let patched = false;
42+
43+
if (product.win32VersionedUpdate) {
44+
delete product.win32VersionedUpdate;
45+
patched = true;
46+
}
47+
// Also check nested objects (e.g. "tunnelApplicationConfig")
48+
for (const key of Object.keys(product)) {
49+
if (typeof product[key] === 'object' && product[key]?.win32VersionedUpdate) {
50+
delete product[key].win32VersionedUpdate;
51+
patched = true;
52+
}
53+
}
54+
55+
if (patched) {
56+
fs.writeFileSync(productJsonPath, JSON.stringify(product, null, '\t') + '\n', 'utf8');
57+
console.log(`[e2e] Patched product.json to skip InnoSetup mutex check: ${productJsonPath}`);
58+
}
59+
} catch (err) {
60+
console.warn(`[e2e] Failed to patch product.json at ${productJsonPath}:`, err);
61+
}
62+
}
63+
}
64+
865
export default async function globalSetup(): Promise<void> {
966
console.log('[e2e] Ensuring VS Code is downloaded...');
1067
const vscodePath = await downloadAndUnzipVSCode({
@@ -13,6 +70,8 @@ export default async function globalSetup(): Promise<void> {
1370
});
1471
console.log(`[e2e] VS Code executable: ${vscodePath}`);
1572

73+
patchProductJsonForWindows(vscodePath);
74+
1675
fs.mkdirSync(VSCODE_CACHE_DIR, { recursive: true });
1776
fs.writeFileSync(VSCODE_PATH_FILE, vscodePath, 'utf8');
1877
}

e2e/helpers/launch.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ export async function launchVSCode(testTitle: string): Promise<VSCodeSession> {
141141
'--skip-release-notes',
142142
'--skip-welcome',
143143
'--no-sandbox',
144+
// Prevent "Code is currently being updated" errors when the host VS Code
145+
// is mid-update — the test instance must not participate in update checks.
146+
'--disable-updates',
144147
// Disable GPU acceleration: prevents Electron GPU-sandbox stalls in headless
145148
// CI environments (required on macOS arm64 runners, harmless elsewhere).
146149
'--disable-gpu',

src/PixelAgentsViewProvider.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,32 @@ export class PixelAgentsViewProvider implements vscode.WebviewViewProvider {
246246
}
247247
})();
248248
sendExistingAgents(this.agents, this.context, this.webview);
249+
} else if (message.type === 'requestDiagnostics') {
250+
// Send connection diagnostics for all agents to the Debug View
251+
const diagnostics: Array<Record<string, unknown>> = [];
252+
for (const [, agent] of this.agents) {
253+
let jsonlExists = false;
254+
let fileSize = 0;
255+
try {
256+
const stat = fs.statSync(agent.jsonlFile);
257+
jsonlExists = true;
258+
fileSize = stat.size;
259+
} catch {
260+
/* file doesn't exist */
261+
}
262+
diagnostics.push({
263+
id: agent.id,
264+
projectDir: agent.projectDir,
265+
projectDirExists: fs.existsSync(agent.projectDir),
266+
jsonlFile: agent.jsonlFile,
267+
jsonlExists,
268+
fileSize,
269+
fileOffset: agent.fileOffset,
270+
lastDataAt: agent.lastDataAt,
271+
linesProcessed: agent.linesProcessed,
272+
});
273+
}
274+
this.webview?.postMessage({ type: 'agentDiagnostics', agents: diagnostics });
249275
} else if (message.type === 'openSessionsFolder') {
250276
const projectDir = getProjectDirPath();
251277
if (projectDir && fs.existsSync(projectDir)) {

src/agentManager.ts

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,34 @@ export function getProjectDirPath(cwd?: string): string {
2424
const dirName = workspacePath.replace(/[^a-zA-Z0-9-]/g, '-');
2525
const projectDir = path.join(os.homedir(), '.claude', 'projects', dirName);
2626
console.log(`[Pixel Agents] Project dir: ${workspacePath}${dirName}`);
27+
28+
// Verify the directory exists; if not, try fuzzy matching against existing dirs
29+
if (!fs.existsSync(projectDir)) {
30+
const projectsRoot = path.join(os.homedir(), '.claude', 'projects');
31+
try {
32+
if (fs.existsSync(projectsRoot)) {
33+
const candidates = fs.readdirSync(projectsRoot);
34+
// Try case-insensitive match (handles Windows drive letter casing)
35+
const lowerDirName = dirName.toLowerCase();
36+
const match = candidates.find((c) => c.toLowerCase() === lowerDirName);
37+
if (match && match !== dirName) {
38+
const matchedDir = path.join(projectsRoot, match);
39+
console.log(
40+
`[Pixel Agents] Project dir not found, using case-insensitive match: ${dirName}${match}`,
41+
);
42+
return matchedDir;
43+
}
44+
if (!match) {
45+
console.warn(
46+
`[Pixel Agents] Project dir does not exist: ${projectDir}. ` +
47+
`Available dirs (${candidates.length}): ${candidates.slice(0, 5).join(', ')}${candidates.length > 5 ? '...' : ''}`,
48+
);
49+
}
50+
}
51+
} catch {
52+
// Ignore scan errors
53+
}
54+
}
2755
return projectDir;
2856
}
2957

@@ -88,6 +116,9 @@ export async function launchNewTerminal(
88116
isWaiting: false,
89117
permissionSent: false,
90118
hadToolsInTurn: false,
119+
lastDataAt: 0,
120+
linesProcessed: 0,
121+
seenUnknownRecordTypes: new Set(),
91122
folderName,
92123
};
93124

@@ -113,11 +144,14 @@ export async function launchNewTerminal(
113144
);
114145

115146
// Poll for the specific JSONL file to appear
147+
let pollCount = 0;
148+
console.log(`[Pixel Agents] Agent ${id}: waiting for JSONL at ${agent.jsonlFile}`);
116149
const pollTimer = setInterval(() => {
150+
pollCount++;
117151
try {
118152
if (fs.existsSync(agent.jsonlFile)) {
119153
console.log(
120-
`[Pixel Agents] Agent ${id}: found JSONL file ${path.basename(agent.jsonlFile)}`,
154+
`[Pixel Agents] Agent ${id}: found JSONL file ${path.basename(agent.jsonlFile)} (after ${pollCount}s)`,
121155
);
122156
clearInterval(pollTimer);
123157
jsonlPollTimers.delete(id);
@@ -132,6 +166,27 @@ export async function launchNewTerminal(
132166
webview,
133167
);
134168
readNewLines(id, agents, waitingTimers, permissionTimers, webview);
169+
} else if (pollCount === 10) {
170+
// After 10s of polling, warn with path details to help diagnose path encoding mismatches
171+
const dirExists = fs.existsSync(projectDir);
172+
let dirContents = '';
173+
if (dirExists) {
174+
try {
175+
const files = fs.readdirSync(projectDir).filter((f) => f.endsWith('.jsonl'));
176+
dirContents =
177+
files.length > 0
178+
? `Dir has ${files.length} JSONL file(s): ${files.slice(0, 3).join(', ')}${files.length > 3 ? '...' : ''}`
179+
: 'Dir exists but has no JSONL files';
180+
} catch {
181+
dirContents = 'Dir exists but unreadable';
182+
}
183+
} else {
184+
dirContents = 'Dir does not exist';
185+
}
186+
console.warn(
187+
`[Pixel Agents] Agent ${id}: JSONL file not found after 10s. ` +
188+
`Expected: ${agent.jsonlFile}. ${dirContents}`,
189+
);
135190
}
136191
} catch {
137192
/* file may not exist yet */
@@ -168,11 +223,6 @@ export function removeAgent(
168223
clearInterval(pt);
169224
}
170225
pollingTimers.delete(agentId);
171-
try {
172-
fs.unwatchFile(agent.jsonlFile);
173-
} catch {
174-
/* ignore */
175-
}
176226

177227
// Cancel timers
178228
cancelWaitingTimer(agentId, waitingTimers);
@@ -244,6 +294,9 @@ export function restoreAgents(
244294
isWaiting: false,
245295
permissionSent: false,
246296
hadToolsInTurn: false,
297+
lastDataAt: 0,
298+
linesProcessed: 0,
299+
seenUnknownRecordTypes: new Set(),
247300
folderName: p.folderName,
248301
};
249302

src/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// ── Timing (ms) ──────────────────────────────────────────────
22
export const JSONL_POLL_INTERVAL_MS = 1000;
3-
export const FILE_WATCHER_POLL_INTERVAL_MS = 1000;
3+
export const FILE_WATCHER_POLL_INTERVAL_MS = 500;
44
export const PROJECT_SCAN_INTERVAL_MS = 1000;
55
export const TOOL_DONE_DELAY_MS = 300;
66
export const PERMISSION_TIMER_DELAY_MS = 7000;

0 commit comments

Comments
 (0)