From 575690ccfba06018243ebdb776bd6251a8d950c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:12:55 +0000 Subject: [PATCH 1/6] Initial plan From 9131475b229e790a747c51b6e224f6f387ad0f4d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:17:34 +0000 Subject: [PATCH 2/6] Add session context change tracking support - Updated SDK to ^0.1.24 (installed 0.1.25) - Added SessionEvent import and re-export in copilot.ts - Extended StreamCallbacks with onSessionEvent callback - Modified sendPrompt to listen for all session events - Updated sendPromptSync to accept and pass session event callback - Added SessionEventWithTask type in executor.ts - Extended ExecutionCallbacks with onSessionEvent callback - Updated executeTask and init task to forward session events - Added session event tracking in execute.tsx - Display current working directory for running tasks - Show context change events in task details Co-authored-by: colindembovsky <1932561+colindembovsky@users.noreply.github.com> --- package-lock.json | 73 ++++++++++++++++------------------- package.json | 2 +- src/screens/execute.tsx | 83 ++++++++++++++++++++++++++++++++++------ src/services/copilot.ts | 20 +++++++++- src/services/executor.ts | 13 +++++++ 5 files changed, 138 insertions(+), 53 deletions(-) diff --git a/package-lock.json b/package-lock.json index 90ab401..9094525 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "planeteer", "version": "0.1.0", "dependencies": { - "@github/copilot-sdk": "^0.1.0", + "@github/copilot-sdk": "^0.1.24", "ink": "^5.1.0", "ink-select-input": "^6.0.0", "ink-spinner": "^5.0.0", @@ -642,26 +642,26 @@ } }, "node_modules/@github/copilot": { - "version": "0.0.403", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-0.0.403.tgz", - "integrity": "sha512-v5jUdtGJReLmE1rmff/LZf+50nzmYQYAaSRNtVNr9g0j0GkCd/noQExe31i1+PudvWU0ZJjltR0B8pUfDRdA9Q==", + "version": "0.0.411", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-0.0.411.tgz", + "integrity": "sha512-I3/7gw40Iu1O+kTyNPKJHNqDRyOebjsUW6wJsvSVrOpT0TNa3/lfm8xdS2XUuJWkp+PgEG/PRwF7u3DVNdP7bQ==", "license": "SEE LICENSE IN LICENSE.md", "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "0.0.403", - "@github/copilot-darwin-x64": "0.0.403", - "@github/copilot-linux-arm64": "0.0.403", - "@github/copilot-linux-x64": "0.0.403", - "@github/copilot-win32-arm64": "0.0.403", - "@github/copilot-win32-x64": "0.0.403" + "@github/copilot-darwin-arm64": "0.0.411", + "@github/copilot-darwin-x64": "0.0.411", + "@github/copilot-linux-arm64": "0.0.411", + "@github/copilot-linux-x64": "0.0.411", + "@github/copilot-win32-arm64": "0.0.411", + "@github/copilot-win32-x64": "0.0.411" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "0.0.403", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-0.0.403.tgz", - "integrity": "sha512-dOw8IleA0d1soHnbr/6wc6vZiYWNTKMgfTe/NET1nCfMzyKDt/0F0I7PT5y+DLujJknTla/ZeEmmBUmliTW4Cg==", + "version": "0.0.411", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-0.0.411.tgz", + "integrity": "sha512-dtr+iHxTS4f8HlV2JT9Fp0FFoxuiPWCnU3XGmrHK+rY6bX5okPC2daU5idvs77WKUGcH8yHTZtfbKYUiMxKosw==", "cpu": [ "arm64" ], @@ -675,9 +675,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "0.0.403", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-0.0.403.tgz", - "integrity": "sha512-aK2jSNWgY8eiZ+TmrvGhssMCPDTKArc0ip6Ul5OaslpytKks8hyXoRbxGD0N9sKioSUSbvKUf+1AqavbDpJO+w==", + "version": "0.0.411", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-0.0.411.tgz", + "integrity": "sha512-zhdbQCbPi1L4iHClackSLx8POfklA+NX9RQLuS48HlKi/0KI/JlaDA/bdbIeMR79wjif5t9gnc/m+RTVmHlRtA==", "cpu": [ "x64" ], @@ -691,9 +691,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "0.0.403", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-0.0.403.tgz", - "integrity": "sha512-KhoR2iR70O6vCkzf0h8/K+p82qAgOvMTgAPm9bVEHvbdGFR7Py9qL5v03bMbPxsA45oNaZAkzDhfTAqWhIAZsQ==", + "version": "0.0.411", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-0.0.411.tgz", + "integrity": "sha512-oZYZ7oX/7O+jzdTUcHkfD1A8YnNRW6mlUgdPjUg+5rXC43bwIdyatAnc0ObY21m9h8ghxGqholoLhm5WnGv1LQ==", "cpu": [ "arm64" ], @@ -707,9 +707,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "0.0.403", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-0.0.403.tgz", - "integrity": "sha512-eoswUc9vo4TB+/9PgFJLVtzI4dPjkpJXdCsAioVuoqPdNxHxlIHFe9HaVcqMRZxUNY1YHEBZozy+IpUEGjgdfQ==", + "version": "0.0.411", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-0.0.411.tgz", + "integrity": "sha512-nnXrKANmmGnkwa3ROlKdAhVNOx8daeMSE8Xh0o3ybKckFv4s38blhKdcxs0RJQRxgAk4p7XXGlDDKNRhurqF1g==", "cpu": [ "x64" ], @@ -723,23 +723,23 @@ } }, "node_modules/@github/copilot-sdk": { - "version": "0.1.22", - "resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-0.1.22.tgz", - "integrity": "sha512-ZGOEBmYOfu/vLXKjjoiw4lO3Cb8QBUuAWXcW/qzmPPsM9+Qe00qVr2AuDTU/Gft9Dm/yZcPK2QuTZc7LVeom9w==", + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-0.1.25.tgz", + "integrity": "sha512-hIgYLPXzWw9bNgrsD5BLKmgVH20ow5Or5UyVXfVe3YgeiaTgFxC4jWSAVHLGB6ufHZUrvbjppcq2dWK63FmDRA==", "license": "MIT", "dependencies": { - "@github/copilot": "^0.0.403", + "@github/copilot": "^0.0.411", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@github/copilot-win32-arm64": { - "version": "0.0.403", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-0.0.403.tgz", - "integrity": "sha512-djWjzCsp2xPNafMyOZ/ivU328/WvWhdroGie/DugiJBTgQL2SP0quWW1fhTlDwE81a3g9CxfJonaRgOpFTJTcg==", + "version": "0.0.411", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-0.0.411.tgz", + "integrity": "sha512-h+Bovb2YVCQSeELZOO7zxv8uht45XHcvAkFbRsc1gf9dl109sSUJIcB4KAhs8Aznk28qksxz7kvdSgUWyQBlIA==", "cpu": [ "arm64" ], @@ -753,9 +753,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "0.0.403", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-0.0.403.tgz", - "integrity": "sha512-lju8cHy2E6Ux7R7tWyLZeksYC2MVZu9i9ocjiBX/qfG2/pNJs7S5OlkwKJ0BSXSbZEHQYq7iHfEWp201bVfk9A==", + "version": "0.0.411", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-0.0.411.tgz", + "integrity": "sha512-xmOgi1lGvUBHQJWmq5AK1EP95+Y8xR4TFoK9OCSOaGbQ+LFcX2jF7iavnMolfWwddabew/AMQjsEHlXvbgMG8Q==", "cpu": [ "x64" ], @@ -1215,7 +1215,6 @@ "integrity": "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1233,7 +1232,6 @@ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -1360,7 +1358,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1833,7 +1830,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2224,7 +2220,6 @@ "resolved": "https://registry.npmjs.org/ink/-/ink-5.2.1.tgz", "integrity": "sha512-BqcUyWrG9zq5HIwW6JcfFHsIYebJkWWb4fczNah1goUO0vv5vneIlfwuS85twyJ5hYR/y18FlAYUxrO9ChIWVg==", "license": "MIT", - "peer": true, "dependencies": { "@alcalzone/ansi-tokenize": "^0.1.3", "ansi-escapes": "^7.0.0", @@ -2730,7 +2725,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2811,7 +2805,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, diff --git a/package.json b/package.json index f6e9673..f1b17cf 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "test:watch": "vitest" }, "dependencies": { - "@github/copilot-sdk": "^0.1.0", + "@github/copilot-sdk": "^0.1.24", "ink": "^5.1.0", "ink-select-input": "^6.0.0", "ink-spinner": "^5.0.0", diff --git a/src/screens/execute.tsx b/src/screens/execute.tsx index 2302ff1..64e6dcc 100644 --- a/src/screens/execute.tsx +++ b/src/screens/execute.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react'; import { Box, Text, useInput } from 'ink'; import type { Plan, Task } from '../models/plan.js'; import { executePlan } from '../services/executor.js'; -import type { ExecutionOptions, ExecutionHandle } from '../services/executor.js'; +import type { ExecutionOptions, ExecutionHandle, SessionEventWithTask } from '../services/executor.js'; import { savePlan, summarizePlan } from '../services/persistence.js'; import { computeBatches } from '../utils/dependency-graph.js'; import Spinner from '../components/spinner.js'; @@ -46,6 +46,8 @@ export default function ExecuteScreen({ const [runCount, setRunCount] = useState(0); // incremented to re-trigger execution const execHandleRef = useRef(null); const [summarized, setSummarized] = useState(''); + const [sessionEvents, setSessionEvents] = useState([]); + const [taskContexts, setTaskContexts] = useState>({}); const { batches } = computeBatches(plan.tasks); // Total display batches: init batch (index 0) + real batches @@ -195,6 +197,28 @@ export default function ExecuteScreen({ } // Otherwise stay on execute screen — user can press 'r' to retry }, + onSessionEvent: (eventWithTask) => { + // Store session events (bounded to prevent memory growth) + setSessionEvents((prev) => { + const updated = [...prev, eventWithTask]; + return updated.slice(-100); // Keep last 100 events + }); + + // Track context changes for each task + if (eventWithTask.event.type === 'session.context_changed') { + const { cwd, repository, branch } = eventWithTask.event.data; + setTaskContexts((prev) => ({ + ...prev, + [eventWithTask.taskId]: { cwd, repository, branch }, + })); + } else if (eventWithTask.event.type === 'session.start' && eventWithTask.event.data.context) { + const { cwd, repository, branch } = eventWithTask.event.data.context; + setTaskContexts((prev) => ({ + ...prev, + [eventWithTask.taskId]: { cwd, repository, branch }, + })); + } + }, }, execOptions); execHandleRef.current = handle; @@ -318,18 +342,29 @@ export default function ExecuteScreen({ const isSelected = i === selectedTaskIndex; const icon = STATUS_ICON[task.status] ?? '?'; const color = STATUS_COLOR[task.status] ?? 'gray'; + const context = taskContexts[task.id]; return ( - - {isSelected ? '❯ ' : ' '} - {icon} - - {task.id} - - — {task.title} - {task.status === 'in_progress' && ( - + + + {isSelected ? '❯ ' : ' '} + {icon} + + {task.id} + + — {task.title} + {task.status === 'in_progress' && ( + + )} + {task.status === 'in_progress' && } + + {context?.cwd && ( + + 📁 {context.cwd} + {context.repository && ( + ({context.repository}) + )} + )} - {task.status === 'in_progress' && } ); })} @@ -376,6 +411,32 @@ export default function ExecuteScreen({ )} + {/* Context change events for selected task */} + {started && selectedTask && (() => { + const taskEvents = sessionEvents.filter( + (e) => e.taskId === selectedTask.id && e.event.type === 'session.context_changed' + ); + if (taskEvents.length === 0) return null; + + return ( + + Context Changes: + {taskEvents.slice(-3).map((e) => { + if (e.event.type !== 'session.context_changed') return null; + const time = new Date(e.event.timestamp).toLocaleTimeString(); + const { cwd, repository, branch } = e.event.data; + return ( + + {time} + → {cwd} + {repository && ({repository}{branch ? `@${branch}` : ''})} + + ); + })} + + ); + })()} + {/* Retry prompt when there are failures */} {started && !executing && failedCount > 0 && ( diff --git a/src/services/copilot.ts b/src/services/copilot.ts index b691e58..fa722df 100644 --- a/src/services/copilot.ts +++ b/src/services/copilot.ts @@ -1,9 +1,13 @@ import { CopilotClient } from '@github/copilot-sdk'; +import type { SessionEvent } from '@github/copilot-sdk'; import { readFile, writeFile, mkdir } from 'node:fs/promises'; import { join } from 'node:path'; import { existsSync } from 'node:fs'; import type { ChatMessage } from '../models/plan.js'; +// Re-export SessionEvent for use in other modules +export type { SessionEvent }; + const SETTINGS_PATH = join(process.cwd(), '.planeteer', 'settings.json'); interface Settings { @@ -108,6 +112,7 @@ export interface StreamCallbacks { onDelta: (text: string) => void; onDone: (fullText: string) => void; onError: (error: Error) => void; + onSessionEvent?: (event: SessionEvent) => void; } export async function sendPrompt( @@ -137,6 +142,13 @@ export async function sendPrompt( let fullText = ''; let settled = false; + // Listen for all session events if callback provided + if (callbacks.onSessionEvent) { + session.on((event) => { + callbacks.onSessionEvent?.(event); + }); + } + session.on('assistant.message_delta', (event: { data: { deltaContent: string } }) => { fullText += event.data.deltaContent; callbacks.onDelta(event.data.deltaContent); @@ -176,10 +188,15 @@ export async function sendPrompt( export async function sendPromptSync( systemPrompt: string, messages: ChatMessage[], - options?: { timeoutMs?: number; onDelta?: (delta: string, fullText: string) => void }, + options?: { + timeoutMs?: number; + onDelta?: (delta: string, fullText: string) => void; + onSessionEvent?: (event: SessionEvent) => void; + }, ): Promise { const idleTimeoutMs = options?.timeoutMs ?? 120_000; const onDelta = options?.onDelta; + const onSessionEvent = options?.onSessionEvent; return new Promise((resolve, reject) => { let settled = false; @@ -235,6 +252,7 @@ export async function sendPromptSync( reject(err); } }, + onSessionEvent, }); }); } diff --git a/src/services/executor.ts b/src/services/executor.ts index 1edf492..09d6b6c 100644 --- a/src/services/executor.ts +++ b/src/services/executor.ts @@ -1,7 +1,13 @@ import type { Plan, Task } from '../models/plan.js'; import { sendPromptSync } from './copilot.js'; +import type { SessionEvent } from './copilot.js'; import { getReadyTasks } from '../utils/dependency-graph.js'; +export interface SessionEventWithTask { + taskId: string; + event: SessionEvent; +} + export interface ExecutionCallbacks { onTaskStart: (taskId: string) => void; onTaskDelta: (taskId: string, delta: string, fullText: string) => void; @@ -9,6 +15,7 @@ export interface ExecutionCallbacks { onTaskFailed: (taskId: string, error: string) => void; onBatchComplete: (batchIndex: number) => void; onAllDone: (plan: Plan) => void; + onSessionEvent?: (eventWithTask: SessionEventWithTask) => void; } function buildTaskPrompt(task: Task, plan: Plan, codebaseContext?: string): string { @@ -112,6 +119,9 @@ export function executePlan( onDelta: (delta, fullText) => { callbacks.onTaskDelta(task.id, delta, fullText); }, + onSessionEvent: (event) => { + callbacks.onSessionEvent?.({ taskId: task.id, event }); + }, }); taskInPlan.status = 'done'; taskInPlan.agentResult = result; @@ -180,6 +190,9 @@ export function executePlan( onDelta: (delta, fullText) => { callbacks.onTaskDelta(INIT_TASK_ID, delta, fullText); }, + onSessionEvent: (event) => { + callbacks.onSessionEvent?.({ taskId: INIT_TASK_ID, event }); + }, }); callbacks.onTaskDone(INIT_TASK_ID, initResult); } catch (err) { From 475f69739b7fc7c3137c57b6e6acd7747abdcfc6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:18:25 +0000 Subject: [PATCH 3/6] Add unit tests for session event tracking - Created executor.test.ts with tests for SessionEventWithTask type - Verified context change events are correctly structured - Tested session.start events with context - Verified ExecutionCallbacks accepts optional onSessionEvent - All tests passing Co-authored-by: colindembovsky <1932561+colindembovsky@users.noreply.github.com> --- src/services/executor.test.ts | 117 ++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 src/services/executor.test.ts diff --git a/src/services/executor.test.ts b/src/services/executor.test.ts new file mode 100644 index 0000000..d0f62e0 --- /dev/null +++ b/src/services/executor.test.ts @@ -0,0 +1,117 @@ +import { describe, it, expect, vi } from 'vitest'; +import type { Plan, Task } from '../models/plan.js'; +import type { ExecutionCallbacks, SessionEventWithTask } from './executor.js'; +import type { SessionEvent } from './copilot.js'; + +describe('SessionEventWithTask type', () => { + it('should correctly structure context change events with task ID', () => { + const mockEvent: SessionEvent = { + id: 'evt-123', + timestamp: new Date().toISOString(), + parentId: null, + type: 'session.context_changed', + data: { + cwd: '/home/user/project', + gitRoot: '/home/user/project', + repository: 'owner/repo', + branch: 'main', + }, + }; + + const eventWithTask: SessionEventWithTask = { + taskId: 'task-1', + event: mockEvent, + }; + + expect(eventWithTask.taskId).toBe('task-1'); + expect(eventWithTask.event.type).toBe('session.context_changed'); + if (eventWithTask.event.type === 'session.context_changed') { + expect(eventWithTask.event.data.cwd).toBe('/home/user/project'); + expect(eventWithTask.event.data.repository).toBe('owner/repo'); + expect(eventWithTask.event.data.branch).toBe('main'); + } + }); + + it('should handle session.start events with context', () => { + const mockEvent: SessionEvent = { + id: 'evt-456', + timestamp: new Date().toISOString(), + parentId: null, + type: 'session.start', + data: { + sessionId: 'sess-123', + version: 1, + producer: 'test', + copilotVersion: '0.1.24', + startTime: new Date().toISOString(), + context: { + cwd: '/workspace', + gitRoot: '/workspace', + repository: 'test/repo', + branch: 'feature', + }, + }, + }; + + const eventWithTask: SessionEventWithTask = { + taskId: 'init-task', + event: mockEvent, + }; + + expect(eventWithTask.taskId).toBe('init-task'); + expect(eventWithTask.event.type).toBe('session.start'); + if (eventWithTask.event.type === 'session.start' && eventWithTask.event.data.context) { + expect(eventWithTask.event.data.context.cwd).toBe('/workspace'); + expect(eventWithTask.event.data.context.repository).toBe('test/repo'); + } + }); +}); + +describe('ExecutionCallbacks with session events', () => { + it('should define onSessionEvent callback as optional', () => { + const callbacks: ExecutionCallbacks = { + onTaskStart: vi.fn(), + onTaskDelta: vi.fn(), + onTaskDone: vi.fn(), + onTaskFailed: vi.fn(), + onBatchComplete: vi.fn(), + onAllDone: vi.fn(), + // onSessionEvent is optional + }; + + expect(callbacks.onSessionEvent).toBeUndefined(); + }); + + it('should accept onSessionEvent callback', () => { + const sessionEventHandler = vi.fn(); + const callbacks: ExecutionCallbacks = { + onTaskStart: vi.fn(), + onTaskDelta: vi.fn(), + onTaskDone: vi.fn(), + onTaskFailed: vi.fn(), + onBatchComplete: vi.fn(), + onAllDone: vi.fn(), + onSessionEvent: sessionEventHandler, + }; + + expect(callbacks.onSessionEvent).toBeDefined(); + expect(typeof callbacks.onSessionEvent).toBe('function'); + + // Test that it can be called with the correct structure + const mockEvent: SessionEvent = { + id: 'evt-789', + timestamp: new Date().toISOString(), + parentId: null, + type: 'session.context_changed', + data: { + cwd: '/test', + }, + }; + + callbacks.onSessionEvent?.({ taskId: 'test-task', event: mockEvent }); + expect(sessionEventHandler).toHaveBeenCalledWith({ + taskId: 'test-task', + event: mockEvent, + }); + }); +}); From fddde888d4e093baabd55b5065ab6f3e24c486b5 Mon Sep 17 00:00:00 2001 From: Colin Dembovsky Date: Wed, 18 Feb 2026 10:54:57 -0600 Subject: [PATCH 4/6] Update src/services/copilot.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/services/copilot.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/services/copilot.ts b/src/services/copilot.ts index fa722df..0960e2a 100644 --- a/src/services/copilot.ts +++ b/src/services/copilot.ts @@ -142,9 +142,13 @@ export async function sendPrompt( let fullText = ''; let settled = false; - // Listen for all session events if callback provided + // Listen for session events if callback provided, but avoid forwarding + // high-volume delta events that are already handled by onDelta. if (callbacks.onSessionEvent) { - session.on((event) => { + session.on((event: SessionEvent) => { + if (event.type === 'assistant.message_delta') { + return; + } callbacks.onSessionEvent?.(event); }); } From 9b49a03a2802ad945a9636bb66b381fe725a59ae Mon Sep 17 00:00:00 2001 From: Colin Dembovsky Date: Wed, 18 Feb 2026 10:55:12 -0600 Subject: [PATCH 5/6] Update src/services/executor.test.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/services/executor.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/executor.test.ts b/src/services/executor.test.ts index d0f62e0..d154c82 100644 --- a/src/services/executor.test.ts +++ b/src/services/executor.test.ts @@ -1,5 +1,4 @@ import { describe, it, expect, vi } from 'vitest'; -import type { Plan, Task } from '../models/plan.js'; import type { ExecutionCallbacks, SessionEventWithTask } from './executor.js'; import type { SessionEvent } from './copilot.js'; From 8cdb876084d16fadfee9140e3dceaf9d35becb08 Mon Sep 17 00:00:00 2001 From: Colin Dembovsky Date: Wed, 18 Feb 2026 10:55:24 -0600 Subject: [PATCH 6/6] Update src/screens/execute.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/screens/execute.tsx | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/screens/execute.tsx b/src/screens/execute.tsx index 64e6dcc..fcf6dba 100644 --- a/src/screens/execute.tsx +++ b/src/screens/execute.tsx @@ -198,24 +198,28 @@ export default function ExecuteScreen({ // Otherwise stay on execute screen — user can press 'r' to retry }, onSessionEvent: (eventWithTask) => { - // Store session events (bounded to prevent memory growth) - setSessionEvents((prev) => { - const updated = [...prev, eventWithTask]; - return updated.slice(-100); // Keep last 100 events - }); + const { event, taskId } = eventWithTask; - // Track context changes for each task - if (eventWithTask.event.type === 'session.context_changed') { - const { cwd, repository, branch } = eventWithTask.event.data; + // Only store session events that we actually render / use, and keep history bounded + if (event.type === 'session.context_changed') { + const { cwd, repository, branch } = event.data; + setSessionEvents((prev) => { + const updated = [...prev, eventWithTask]; + return updated.slice(-100); // Keep last 100 events + }); setTaskContexts((prev) => ({ ...prev, - [eventWithTask.taskId]: { cwd, repository, branch }, + [taskId]: { cwd, repository, branch }, })); - } else if (eventWithTask.event.type === 'session.start' && eventWithTask.event.data.context) { - const { cwd, repository, branch } = eventWithTask.event.data.context; + } else if (event.type === 'session.start' && event.data.context) { + const { cwd, repository, branch } = event.data.context; + setSessionEvents((prev) => { + const updated = [...prev, eventWithTask]; + return updated.slice(-100); // Keep last 100 events + }); setTaskContexts((prev) => ({ ...prev, - [eventWithTask.taskId]: { cwd, repository, branch }, + [taskId]: { cwd, repository, branch }, })); } },