Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 66 additions & 3 deletions src/PixelAgentsViewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ import {
sendWallTilesToWebview,
} from './assetLoader.js';
import { GLOBAL_KEY_SOUND_ENABLED, WORKSPACE_KEY_AGENT_SEATS } from './constants.js';
import { ensureProjectScan } from './fileWatcher.js';
import {
ensureProjectScan,
startExternalSessionScanning,
startStaleExternalAgentCheck,
} from './fileWatcher.js';
import type { LayoutWatcher } from './layoutPersistence.js';
import { readLayoutFromFile, watchLayoutFile, writeLayoutToFile } from './layoutPersistence.js';
import type { AgentState } from './types.js';
Expand All @@ -47,6 +51,10 @@ export class PixelAgentsViewProvider implements vscode.WebviewViewProvider {
knownJsonlFiles = new Set<string>();
projectScanTimer = { current: null as ReturnType<typeof setInterval> | null };

// External session detection (VS Code extension panel, etc.)
externalScanTimer: ReturnType<typeof setInterval> | null = null;
staleCheckTimer: ReturnType<typeof setInterval> | null = null;

// Bundled default layout (loaded from assets/default-layout.json)
defaultLayout: Record<string, unknown> | null = null;

Expand Down Expand Up @@ -93,12 +101,30 @@ export class PixelAgentsViewProvider implements vscode.WebviewViewProvider {
} else if (message.type === 'focusAgent') {
const agent = this.agents.get(message.id);
if (agent) {
agent.terminalRef.show();
if (agent.terminalRef) {
agent.terminalRef.show();
}
// External agents (extension panel) have no terminal to focus
}
} else if (message.type === 'closeAgent') {
const agent = this.agents.get(message.id);
if (agent) {
agent.terminalRef.dispose();
if (agent.terminalRef) {
agent.terminalRef.dispose();
} else {
// External agent — just remove from tracking
removeAgent(
message.id,
this.agents,
this.fileWatchers,
this.pollingTimers,
this.waitingTimers,
this.permissionTimers,
this.jsonlPollTimers,
this.persistAgents,
);
webviewView.webview.postMessage({ type: 'agentClosed', id: message.id });
}
}
} else if (message.type === 'saveAgentSeats') {
// Store seat assignments in a separate key (never touched by persistAgents)
Expand Down Expand Up @@ -160,6 +186,35 @@ export class PixelAgentsViewProvider implements vscode.WebviewViewProvider {
this.persistAgents,
);

// Start external session scanning (detects VS Code extension panel sessions)
if (!this.externalScanTimer) {
this.externalScanTimer = startExternalSessionScanning(
projectDir,
this.knownJsonlFiles,
this.nextAgentId,
this.agents,
this.fileWatchers,
this.pollingTimers,
this.waitingTimers,
this.permissionTimers,
this.jsonlPollTimers,
this.webview,
this.persistAgents,
);
}
if (!this.staleCheckTimer) {
this.staleCheckTimer = startStaleExternalAgentCheck(
this.agents,
this.fileWatchers,
this.pollingTimers,
this.waitingTimers,
this.permissionTimers,
this.jsonlPollTimers,
this.webview,
this.persistAgents,
);
}

// Load furniture assets BEFORE sending layout
(async () => {
try {
Expand Down Expand Up @@ -388,6 +443,14 @@ export class PixelAgentsViewProvider implements vscode.WebviewViewProvider {
clearInterval(this.projectScanTimer.current);
this.projectScanTimer.current = null;
}
if (this.externalScanTimer) {
clearInterval(this.externalScanTimer);
this.externalScanTimer = null;
}
if (this.staleCheckTimer) {
clearInterval(this.staleCheckTimer);
this.staleCheckTimer = null;
}
}
}

Expand Down
37 changes: 32 additions & 5 deletions src/agentManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export async function launchNewTerminal(
const agent: AgentState = {
id,
terminalRef: terminal,
isExternal: false,
projectDir,
jsonlFile: expectedFile,
fileOffset: 0,
Expand Down Expand Up @@ -183,7 +184,8 @@ export function persistAgents(
for (const agent of agents.values()) {
persisted.push({
id: agent.id,
terminalName: agent.terminalRef.name,
terminalName: agent.terminalRef?.name ?? '',
isExternal: agent.isExternal || undefined,
jsonlFile: agent.jsonlFile,
projectDir: agent.projectDir,
folderName: agent.folderName,
Expand Down Expand Up @@ -217,12 +219,28 @@ export function restoreAgents(
let restoredProjectDir: string | null = null;

for (const p of persisted) {
const terminal = liveTerminals.find((t) => t.name === p.terminalName);
if (!terminal) continue;
let terminal: vscode.Terminal | undefined;
const isExternal = p.isExternal ?? false;

if (isExternal) {
// External agents (extension panel sessions) — restore if JSONL file was recently active
try {
if (!fs.existsSync(p.jsonlFile)) continue;
const stat = fs.statSync(p.jsonlFile);
if (Date.now() - stat.mtimeMs > 300_000) continue; // Skip if stale (>5 min)
} catch {
continue;
}
} else {
// Terminal agents — find matching terminal by name
terminal = liveTerminals.find((t) => t.name === p.terminalName);
if (!terminal) continue;
}

const agent: AgentState = {
id: p.id,
terminalRef: terminal,
isExternal,
projectDir: p.projectDir,
jsonlFile: p.jsonlFile,
fileOffset: 0,
Expand All @@ -240,7 +258,11 @@ export function restoreAgents(

agents.set(p.id, agent);
knownJsonlFiles.add(p.jsonlFile);
console.log(`[Pixel Agents] Restored agent ${p.id} → terminal "${p.terminalName}"`);
if (isExternal) {
console.log(`[Pixel Agents] Restored external agent ${p.id} → ${path.basename(p.jsonlFile)}`);
} else {
console.log(`[Pixel Agents] Restored agent ${p.id} → terminal "${p.terminalName}"`);
}

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

// Include folderName per agent
// Include folderName and isExternal per agent
const folderNames: Record<number, string> = {};
const externalAgents: Record<number, boolean> = {};
for (const [id, agent] of agents) {
if (agent.folderName) {
folderNames[id] = agent.folderName;
}
if (agent.isExternal) {
externalAgents[id] = true;
}
}
console.log(
`[Pixel Agents] sendExistingAgents: agents=${JSON.stringify(agentIds)}, meta=${JSON.stringify(agentMeta)}`,
Expand All @@ -362,6 +388,7 @@ export function sendExistingAgents(
agents: agentIds,
agentMeta,
folderNames,
externalAgents,
});

sendCurrentAgentStatuses(agents, webview);
Expand Down
8 changes: 8 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ export const TOOL_DONE_DELAY_MS = 300;
export const PERMISSION_TIMER_DELAY_MS = 7000;
export const TEXT_IDLE_DELAY_MS = 5000;

// ── External Session Detection (VS Code extension panel, etc.) ──
export const EXTERNAL_SCAN_INTERVAL_MS = 5000;
/** Only adopt JSONL files modified within this window */
export const EXTERNAL_ACTIVE_THRESHOLD_MS = 30_000;
/** Remove external agents after this much inactivity */
export const EXTERNAL_STALE_TIMEOUT_MS = 300_000; // 5 minutes
export const EXTERNAL_STALE_CHECK_INTERVAL_MS = 30_000;

// ── Display Truncation ──────────────────────────────────────
export const BASH_COMMAND_DISPLAY_MAX_LENGTH = 30;
export const TASK_DESCRIPTION_DISPLAY_MAX_LENGTH = 40;
Expand Down
Loading