Skip to content

Commit d45bafc

Browse files
committed
feat: add external session support and Agent tool recognition
Detect and track Claude Code sessions running in the VS Code extension panel (WebSocket transport, no terminal). These produce JSONL transcripts like terminal sessions but have no associated Terminal object. Changes: - Make terminalRef optional, add isExternal flag to AgentState - Add external session scanning (5s interval) and stale cleanup (5min timeout) - Persist/restore external agents across reloads - Guard terminal-specific code paths (focus, close, /clear reassignment) - Recognize renamed 'Agent' tool alongside 'Task' for sub-agents Known limitation: external sessions rely on JSONL file mtime for stale detection (no close event available), so agents linger up to 5 minutes after the extension panel session ends. Supersedes pablodelucca#76 and pablodelucca#77.
1 parent 20cbf80 commit d45bafc

File tree

6 files changed

+332
-19
lines changed

6 files changed

+332
-19
lines changed

src/PixelAgentsViewProvider.ts

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ import {
2424
sendWallTilesToWebview,
2525
} from './assetLoader.js';
2626
import { GLOBAL_KEY_SOUND_ENABLED, WORKSPACE_KEY_AGENT_SEATS } from './constants.js';
27-
import { ensureProjectScan } from './fileWatcher.js';
27+
import {
28+
ensureProjectScan,
29+
startExternalSessionScanning,
30+
startStaleExternalAgentCheck,
31+
} from './fileWatcher.js';
2832
import type { LayoutWatcher } from './layoutPersistence.js';
2933
import { readLayoutFromFile, watchLayoutFile, writeLayoutToFile } from './layoutPersistence.js';
3034
import type { AgentState } from './types.js';
@@ -47,6 +51,10 @@ export class PixelAgentsViewProvider implements vscode.WebviewViewProvider {
4751
knownJsonlFiles = new Set<string>();
4852
projectScanTimer = { current: null as ReturnType<typeof setInterval> | null };
4953

54+
// External session detection (VS Code extension panel, etc.)
55+
externalScanTimer: ReturnType<typeof setInterval> | null = null;
56+
staleCheckTimer: ReturnType<typeof setInterval> | null = null;
57+
5058
// Bundled default layout (loaded from assets/default-layout.json)
5159
defaultLayout: Record<string, unknown> | null = null;
5260

@@ -93,12 +101,30 @@ export class PixelAgentsViewProvider implements vscode.WebviewViewProvider {
93101
} else if (message.type === 'focusAgent') {
94102
const agent = this.agents.get(message.id);
95103
if (agent) {
96-
agent.terminalRef.show();
104+
if (agent.terminalRef) {
105+
agent.terminalRef.show();
106+
}
107+
// External agents (extension panel) have no terminal to focus
97108
}
98109
} else if (message.type === 'closeAgent') {
99110
const agent = this.agents.get(message.id);
100111
if (agent) {
101-
agent.terminalRef.dispose();
112+
if (agent.terminalRef) {
113+
agent.terminalRef.dispose();
114+
} else {
115+
// External agent — just remove from tracking
116+
removeAgent(
117+
message.id,
118+
this.agents,
119+
this.fileWatchers,
120+
this.pollingTimers,
121+
this.waitingTimers,
122+
this.permissionTimers,
123+
this.jsonlPollTimers,
124+
this.persistAgents,
125+
);
126+
webviewView.webview.postMessage({ type: 'agentClosed', id: message.id });
127+
}
102128
}
103129
} else if (message.type === 'saveAgentSeats') {
104130
// Store seat assignments in a separate key (never touched by persistAgents)
@@ -160,6 +186,35 @@ export class PixelAgentsViewProvider implements vscode.WebviewViewProvider {
160186
this.persistAgents,
161187
);
162188

189+
// Start external session scanning (detects VS Code extension panel sessions)
190+
if (!this.externalScanTimer) {
191+
this.externalScanTimer = startExternalSessionScanning(
192+
projectDir,
193+
this.knownJsonlFiles,
194+
this.nextAgentId,
195+
this.agents,
196+
this.fileWatchers,
197+
this.pollingTimers,
198+
this.waitingTimers,
199+
this.permissionTimers,
200+
this.jsonlPollTimers,
201+
this.webview,
202+
this.persistAgents,
203+
);
204+
}
205+
if (!this.staleCheckTimer) {
206+
this.staleCheckTimer = startStaleExternalAgentCheck(
207+
this.agents,
208+
this.fileWatchers,
209+
this.pollingTimers,
210+
this.waitingTimers,
211+
this.permissionTimers,
212+
this.jsonlPollTimers,
213+
this.webview,
214+
this.persistAgents,
215+
);
216+
}
217+
163218
// Load furniture assets BEFORE sending layout
164219
(async () => {
165220
try {
@@ -388,6 +443,14 @@ export class PixelAgentsViewProvider implements vscode.WebviewViewProvider {
388443
clearInterval(this.projectScanTimer.current);
389444
this.projectScanTimer.current = null;
390445
}
446+
if (this.externalScanTimer) {
447+
clearInterval(this.externalScanTimer);
448+
this.externalScanTimer = null;
449+
}
450+
if (this.staleCheckTimer) {
451+
clearInterval(this.staleCheckTimer);
452+
this.staleCheckTimer = null;
453+
}
391454
}
392455
}
393456

src/agentManager.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export async function launchNewTerminal(
6868
const agent: AgentState = {
6969
id,
7070
terminalRef: terminal,
71+
isExternal: false,
7172
projectDir,
7273
jsonlFile: expectedFile,
7374
fileOffset: 0,
@@ -183,7 +184,8 @@ export function persistAgents(
183184
for (const agent of agents.values()) {
184185
persisted.push({
185186
id: agent.id,
186-
terminalName: agent.terminalRef.name,
187+
terminalName: agent.terminalRef?.name ?? '',
188+
isExternal: agent.isExternal || undefined,
187189
jsonlFile: agent.jsonlFile,
188190
projectDir: agent.projectDir,
189191
folderName: agent.folderName,
@@ -217,12 +219,28 @@ export function restoreAgents(
217219
let restoredProjectDir: string | null = null;
218220

219221
for (const p of persisted) {
220-
const terminal = liveTerminals.find((t) => t.name === p.terminalName);
221-
if (!terminal) continue;
222+
let terminal: vscode.Terminal | undefined;
223+
const isExternal = p.isExternal ?? false;
224+
225+
if (isExternal) {
226+
// External agents (extension panel sessions) — restore if JSONL file was recently active
227+
try {
228+
if (!fs.existsSync(p.jsonlFile)) continue;
229+
const stat = fs.statSync(p.jsonlFile);
230+
if (Date.now() - stat.mtimeMs > 300_000) continue; // Skip if stale (>5 min)
231+
} catch {
232+
continue;
233+
}
234+
} else {
235+
// Terminal agents — find matching terminal by name
236+
terminal = liveTerminals.find((t) => t.name === p.terminalName);
237+
if (!terminal) continue;
238+
}
222239

223240
const agent: AgentState = {
224241
id: p.id,
225242
terminalRef: terminal,
243+
isExternal,
226244
projectDir: p.projectDir,
227245
jsonlFile: p.jsonlFile,
228246
fileOffset: 0,
@@ -240,7 +258,11 @@ export function restoreAgents(
240258

241259
agents.set(p.id, agent);
242260
knownJsonlFiles.add(p.jsonlFile);
243-
console.log(`[Pixel Agents] Restored agent ${p.id} → terminal "${p.terminalName}"`);
261+
if (isExternal) {
262+
console.log(`[Pixel Agents] Restored external agent ${p.id}${path.basename(p.jsonlFile)}`);
263+
} else {
264+
console.log(`[Pixel Agents] Restored agent ${p.id} → terminal "${p.terminalName}"`);
265+
}
244266

245267
if (p.id > maxId) maxId = p.id;
246268
// Extract terminal index from name like "Claude Code #3"
@@ -346,12 +368,16 @@ export function sendExistingAgents(
346368
Record<string, { palette?: number; seatId?: string }>
347369
>(WORKSPACE_KEY_AGENT_SEATS, {});
348370

349-
// Include folderName per agent
371+
// Include folderName and isExternal per agent
350372
const folderNames: Record<number, string> = {};
373+
const externalAgents: Record<number, boolean> = {};
351374
for (const [id, agent] of agents) {
352375
if (agent.folderName) {
353376
folderNames[id] = agent.folderName;
354377
}
378+
if (agent.isExternal) {
379+
externalAgents[id] = true;
380+
}
355381
}
356382
console.log(
357383
`[Pixel Agents] sendExistingAgents: agents=${JSON.stringify(agentIds)}, meta=${JSON.stringify(agentMeta)}`,
@@ -362,6 +388,7 @@ export function sendExistingAgents(
362388
agents: agentIds,
363389
agentMeta,
364390
folderNames,
391+
externalAgents,
365392
});
366393

367394
sendCurrentAgentStatuses(agents, webview);

src/constants.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ export const TOOL_DONE_DELAY_MS = 300;
66
export const PERMISSION_TIMER_DELAY_MS = 7000;
77
export const TEXT_IDLE_DELAY_MS = 5000;
88

9+
// ── External Session Detection (VS Code extension panel, etc.) ──
10+
export const EXTERNAL_SCAN_INTERVAL_MS = 5000;
11+
/** Only adopt JSONL files modified within this window */
12+
export const EXTERNAL_ACTIVE_THRESHOLD_MS = 30_000;
13+
/** Remove external agents after this much inactivity */
14+
export const EXTERNAL_STALE_TIMEOUT_MS = 300_000; // 5 minutes
15+
export const EXTERNAL_STALE_CHECK_INTERVAL_MS = 30_000;
16+
917
// ── Display Truncation ──────────────────────────────────────
1018
export const BASH_COMMAND_DISPLAY_MAX_LENGTH = 30;
1119
export const TASK_DESCRIPTION_DISPLAY_MAX_LENGTH = 40;

0 commit comments

Comments
 (0)