diff --git a/package-lock.json b/package-lock.json
index 9094525..99c2ead 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.24",
+ "@github/copilot-sdk": "^0.1.26",
"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.411",
- "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-0.0.411.tgz",
- "integrity": "sha512-I3/7gw40Iu1O+kTyNPKJHNqDRyOebjsUW6wJsvSVrOpT0TNa3/lfm8xdS2XUuJWkp+PgEG/PRwF7u3DVNdP7bQ==",
+ "version": "0.0.414",
+ "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-0.0.414.tgz",
+ "integrity": "sha512-jseJ2S02CLWrFks5QK22zzq7as2ErY5m1wMCFBOE6sro1uACq1kvqqM1LwM4qy58YSZFrM1ZAn1s7UOVd9zhIA==",
"license": "SEE LICENSE IN LICENSE.md",
"bin": {
"copilot": "npm-loader.js"
},
"optionalDependencies": {
- "@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"
+ "@github/copilot-darwin-arm64": "0.0.414",
+ "@github/copilot-darwin-x64": "0.0.414",
+ "@github/copilot-linux-arm64": "0.0.414",
+ "@github/copilot-linux-x64": "0.0.414",
+ "@github/copilot-win32-arm64": "0.0.414",
+ "@github/copilot-win32-x64": "0.0.414"
}
},
"node_modules/@github/copilot-darwin-arm64": {
- "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==",
+ "version": "0.0.414",
+ "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-0.0.414.tgz",
+ "integrity": "sha512-PW4v89v41i4Mg/NYl4+gEhwnDaVz+olNL+RbqtiQI3IV89gZdS+RZQbUEJfOwMaFcT2GfiUK1OuB+YDv5GrkBg==",
"cpu": [
"arm64"
],
@@ -675,9 +675,9 @@
}
},
"node_modules/@github/copilot-darwin-x64": {
- "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==",
+ "version": "0.0.414",
+ "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-0.0.414.tgz",
+ "integrity": "sha512-NyPYm0NovQTwtuI42WJIi4cjYd2z0wBHEvWlUSczRsSuYEyImAClmZmBPegUU63e5JdZd1PdQkQ7FqrrfL2fZQ==",
"cpu": [
"x64"
],
@@ -691,9 +691,9 @@
}
},
"node_modules/@github/copilot-linux-arm64": {
- "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==",
+ "version": "0.0.414",
+ "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-0.0.414.tgz",
+ "integrity": "sha512-VgdRsvA1FiZ1lcU/AscSvyJWEUWZzoXv2tSZ6WV3NE0uUTuO1Qoq4xuqbKZ/+vKJmn1b8afe7sxAAOtCoWPBHQ==",
"cpu": [
"arm64"
],
@@ -707,9 +707,9 @@
}
},
"node_modules/@github/copilot-linux-x64": {
- "version": "0.0.411",
- "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-0.0.411.tgz",
- "integrity": "sha512-nnXrKANmmGnkwa3ROlKdAhVNOx8daeMSE8Xh0o3ybKckFv4s38blhKdcxs0RJQRxgAk4p7XXGlDDKNRhurqF1g==",
+ "version": "0.0.414",
+ "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-0.0.414.tgz",
+ "integrity": "sha512-3HyZsbZqYTF5jcT7/e+nDIYBCQXo8UCVWjBI3raOE4lzAw9b2ucL290IhtA23s1+EiquMxJ4m3FnjwFmwlQ12A==",
"cpu": [
"x64"
],
@@ -723,12 +723,12 @@
}
},
"node_modules/@github/copilot-sdk": {
- "version": "0.1.25",
- "resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-0.1.25.tgz",
- "integrity": "sha512-hIgYLPXzWw9bNgrsD5BLKmgVH20ow5Or5UyVXfVe3YgeiaTgFxC4jWSAVHLGB6ufHZUrvbjppcq2dWK63FmDRA==",
+ "version": "0.1.26",
+ "resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-0.1.26.tgz",
+ "integrity": "sha512-5YsApwYa/k2VqaNGvB+ngvWIRxyBR+AY17wCX3ceo+0UcwR7RbW0Ld8l8c5wFOh8Qa/UN4/kXb10HRUTYelGUA==",
"license": "MIT",
"dependencies": {
- "@github/copilot": "^0.0.411",
+ "@github/copilot": "^0.0.414",
"vscode-jsonrpc": "^8.2.1",
"zod": "^4.3.6"
},
@@ -737,9 +737,9 @@
}
},
"node_modules/@github/copilot-win32-arm64": {
- "version": "0.0.411",
- "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-0.0.411.tgz",
- "integrity": "sha512-h+Bovb2YVCQSeELZOO7zxv8uht45XHcvAkFbRsc1gf9dl109sSUJIcB4KAhs8Aznk28qksxz7kvdSgUWyQBlIA==",
+ "version": "0.0.414",
+ "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-0.0.414.tgz",
+ "integrity": "sha512-8gdaoF4MPpeV0h8UnCZ8TKI5l274EP0fvAaV9BGjsdyEydDcEb+DHqQiXgitWVKKiHAAaPi12aH8P5OsEDUneQ==",
"cpu": [
"arm64"
],
@@ -753,9 +753,9 @@
}
},
"node_modules/@github/copilot-win32-x64": {
- "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==",
+ "version": "0.0.414",
+ "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-0.0.414.tgz",
+ "integrity": "sha512-E1Oq1jXHaL1oWNsmmiCd4G30/CfgVdswg/T5oDFUxx3Ri+6uBekciIzdyCDelsP1kn2+fC1EYz2AerQ6F+huzg==",
"cpu": [
"x64"
],
diff --git a/package.json b/package.json
index f1b17cf..4dc48bf 100644
--- a/package.json
+++ b/package.json
@@ -15,7 +15,7 @@
"test:watch": "vitest"
},
"dependencies": {
- "@github/copilot-sdk": "^0.1.24",
+ "@github/copilot-sdk": "^0.1.26",
"ink": "^5.1.0",
"ink-select-input": "^6.0.0",
"ink-spinner": "^5.0.0",
diff --git a/src/components/status-bar.tsx b/src/components/status-bar.tsx
index 7bbc89a..1e1b471 100644
--- a/src/components/status-bar.tsx
+++ b/src/components/status-bar.tsx
@@ -7,16 +7,18 @@ interface StatusBarProps {
screen: string;
hint?: string;
model?: string;
+ extra?: string;
}
-export default function StatusBar({ screen, hint, model }: StatusBarProps): React.ReactElement {
+export default function StatusBar({ screen, hint, model, extra }: StatusBarProps): React.ReactElement {
const displayModel = model || getModelLabel();
const { stdout } = useStdout();
// Parent App uses padding={1} → 2 cols consumed; border chars │…│ take 2 more
const innerWidth = (stdout?.columns ?? 80) - 4;
- // Build right-side content: "model hint q: quit"
+ // Build right-side content: "model extra hint q: quit"
const rightParts: string[] = [displayModel];
+ if (extra) rightParts.push(extra);
if (hint) rightParts.push(hint);
rightParts.push('q: quit');
const rightText = rightParts.join(' ');
diff --git a/src/screens/clarify.tsx b/src/screens/clarify.tsx
index 1b0f349..88e1b20 100644
--- a/src/screens/clarify.tsx
+++ b/src/screens/clarify.tsx
@@ -26,6 +26,7 @@ export default function ClarifyScreen({ onScopeConfirmed, onBack }: ClarifyScree
const [codebaseContext, setCodebaseContext] = useState('');
const [inspecting, setInspecting] = useState(false);
const [inspectDone, setInspectDone] = useState(false);
+ const [compactionMsg, setCompactionMsg] = useState('');
useEffect(() => {
loadHistory().then(setHistory);
@@ -108,6 +109,19 @@ export default function ClarifyScreen({ onScopeConfirmed, onBack }: ClarifyScree
]);
setStreaming(false);
},
+ onSessionEvent: (event) => {
+ if (event.type === 'session.compaction_start') {
+ setCompactionMsg('⟳ Compacting session history…');
+ } else if (event.type === 'session.compaction_complete') {
+ if (event.data.success) {
+ const removed = event.data.messagesRemoved ?? 0;
+ setCompactionMsg(`✓ Session history compacted (${removed} messages removed)`);
+ } else {
+ setCompactionMsg(`⚠ Compaction failed: ${event.data.error ?? 'unknown error'}`);
+ }
+ setTimeout(() => setCompactionMsg(''), 5000);
+ }
+ },
}, codebaseContext || undefined).catch((error: Error) => {
setMessages((prev) => [
...prev,
@@ -232,6 +246,13 @@ export default function ClarifyScreen({ onScopeConfirmed, onBack }: ClarifyScree
)}
+ {/* Compaction notification */}
+ {compactionMsg !== '' && (
+
+ {compactionMsg}
+
+ )}
+
([]);
const [taskContexts, setTaskContexts] = useState>({});
+ const [tokenUsage, setTokenUsage] = useState<{ tokenLimit: number; currentTokens: number } | null>(null);
+ const [compactionMsg, setCompactionMsg] = useState('');
const { batches } = computeBatches(plan.tasks);
// Total display batches: init batch (index 0) + real batches
@@ -211,6 +213,20 @@ export default function ExecuteScreen({
...prev,
[taskId]: { cwd, repository, branch },
}));
+ } else if (event.type === 'session.usage_info') {
+ setTokenUsage({ tokenLimit: event.data.tokenLimit, currentTokens: event.data.currentTokens });
+ } else if (event.type === 'session.compaction_start') {
+ setCompactionMsg('⟳ Compacting session history…');
+ } else if (event.type === 'session.compaction_complete') {
+ if (event.data.success) {
+ const removed = event.data.messagesRemoved ?? 0;
+ const pre = event.data.preCompactionTokens ?? 0;
+ const post = event.data.postCompactionTokens ?? 0;
+ setCompactionMsg(`✓ Compacted: ${removed} messages removed (${pre}→${post} tokens)`);
+ } else {
+ setCompactionMsg(`⚠ Compaction failed: ${event.data.error ?? 'unknown error'}`);
+ }
+ setTimeout(() => setCompactionMsg(''), 5000);
}
},
}, execOptions);
@@ -456,6 +472,13 @@ export default function ExecuteScreen({
)}
+ {/* Compaction notification */}
+ {compactionMsg !== '' && (
+
+ {compactionMsg}
+
+ )}
+
);
diff --git a/src/screens/refine.tsx b/src/screens/refine.tsx
index 57aeb71..c03c353 100644
--- a/src/screens/refine.tsx
+++ b/src/screens/refine.tsx
@@ -41,6 +41,7 @@ export default function RefineScreen({
const [viewMode, setViewMode] = useState('tree');
const [editingTask, setEditingTask] = useState(null);
const [commandMode, setCommandMode] = useState(false);
+ const [compactionMsg, setCompactionMsg] = useState('');
const toggleSkill = useCallback(
(skillName: string) => {
@@ -164,7 +165,19 @@ export default function RefineScreen({
.then((skillOptions) =>
refineWBS(currentPlan.tasks, value, (_delta, fullText) => {
setStreamText(fullText);
- }, skillOptions)
+ }, skillOptions, (event) => {
+ if (event.type === 'session.compaction_start') {
+ setCompactionMsg('⟳ Compacting session history…');
+ } else if (event.type === 'session.compaction_complete') {
+ if (event.data.success) {
+ const removed = event.data.messagesRemoved ?? 0;
+ setCompactionMsg(`✓ Session history compacted (${removed} messages removed)`);
+ } else {
+ setCompactionMsg(`⚠ Compaction failed: ${event.data.error ?? 'unknown error'}`);
+ }
+ setTimeout(() => setCompactionMsg(''), 5000);
+ }
+ })
)
.then((tasks) => {
const updated = { ...currentPlan, tasks, updatedAt: new Date().toISOString() };
@@ -301,6 +314,13 @@ export default function RefineScreen({
{saved && ✓ Plan saved}
+ {/* Compaction notification */}
+ {compactionMsg !== '' && (
+
+ {compactionMsg}
+
+ )}
+
{
}
}
+/** Default context utilization threshold (0.0–1.0) at which background compaction starts. */
+export const COMPACTION_THRESHOLD = 0.8;
+
export interface StreamCallbacks {
onDelta: (text: string) => void;
onDone: (fullText: string) => void;
@@ -205,11 +208,13 @@ export async function sendPrompt(
streaming: boolean;
skillDirectories?: string[];
disabledSkills?: string[];
+ infiniteSessions?: { backgroundCompactionThreshold?: number };
}
const sessionConfig: SessionConfigWithSkills = {
model: currentModel,
streaming: true,
+ infiniteSessions: { backgroundCompactionThreshold: COMPACTION_THRESHOLD },
};
if (skillOptions?.skillDirectories && skillOptions.skillDirectories.length > 0) {
diff --git a/src/services/executor.test.ts b/src/services/executor.test.ts
index d154c82..b5778b2 100644
--- a/src/services/executor.test.ts
+++ b/src/services/executor.test.ts
@@ -1,6 +1,7 @@
import { describe, it, expect, vi } from 'vitest';
import type { ExecutionCallbacks, SessionEventWithTask } from './executor.js';
import type { SessionEvent } from './copilot.js';
+import { COMPACTION_THRESHOLD } from './copilot.js';
describe('SessionEventWithTask type', () => {
it('should correctly structure context change events with task ID', () => {
@@ -64,6 +65,81 @@ describe('SessionEventWithTask type', () => {
expect(eventWithTask.event.data.context.repository).toBe('test/repo');
}
});
+
+ it('should handle session.usage_info events with task ID', () => {
+ const mockEvent: SessionEvent = {
+ id: 'evt-usage-1',
+ timestamp: new Date().toISOString(),
+ parentId: null,
+ ephemeral: true,
+ type: 'session.usage_info',
+ data: {
+ tokenLimit: 128000,
+ currentTokens: 102400,
+ messagesLength: 42,
+ },
+ };
+
+ const eventWithTask: SessionEventWithTask = {
+ taskId: 'task-abc',
+ event: mockEvent,
+ };
+
+ expect(eventWithTask.taskId).toBe('task-abc');
+ expect(eventWithTask.event.type).toBe('session.usage_info');
+ if (eventWithTask.event.type === 'session.usage_info') {
+ expect(eventWithTask.event.data.tokenLimit).toBe(128000);
+ expect(eventWithTask.event.data.currentTokens).toBe(102400);
+ expect(eventWithTask.event.data.messagesLength).toBe(42);
+ }
+ });
+
+ it('should handle session.compaction_start events', () => {
+ const mockEvent: SessionEvent = {
+ id: 'evt-compact-start',
+ timestamp: new Date().toISOString(),
+ parentId: null,
+ type: 'session.compaction_start',
+ data: {},
+ };
+
+ const eventWithTask: SessionEventWithTask = {
+ taskId: 'task-xyz',
+ event: mockEvent,
+ };
+
+ expect(eventWithTask.taskId).toBe('task-xyz');
+ expect(eventWithTask.event.type).toBe('session.compaction_start');
+ });
+
+ it('should handle session.compaction_complete events', () => {
+ const mockEvent: SessionEvent = {
+ id: 'evt-compact-done',
+ timestamp: new Date().toISOString(),
+ parentId: null,
+ type: 'session.compaction_complete',
+ data: {
+ success: true,
+ preCompactionTokens: 102400,
+ postCompactionTokens: 8192,
+ messagesRemoved: 45,
+ tokensRemoved: 94208,
+ },
+ };
+
+ const eventWithTask: SessionEventWithTask = {
+ taskId: 'task-xyz',
+ event: mockEvent,
+ };
+
+ expect(eventWithTask.event.type).toBe('session.compaction_complete');
+ if (eventWithTask.event.type === 'session.compaction_complete') {
+ expect(eventWithTask.event.data.success).toBe(true);
+ expect(eventWithTask.event.data.messagesRemoved).toBe(45);
+ expect(eventWithTask.event.data.preCompactionTokens).toBe(102400);
+ expect(eventWithTask.event.data.postCompactionTokens).toBe(8192);
+ }
+ });
});
describe('ExecutionCallbacks with session events', () => {
@@ -113,4 +189,60 @@ describe('ExecutionCallbacks with session events', () => {
event: mockEvent,
});
});
+
+ it('should receive and forward compaction events via onSessionEvent', () => {
+ 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,
+ };
+
+ const compactionStartEvent: SessionEvent = {
+ id: 'compact-start-1',
+ timestamp: new Date().toISOString(),
+ parentId: null,
+ type: 'session.compaction_start',
+ data: {},
+ };
+
+ const compactionCompleteEvent: SessionEvent = {
+ id: 'compact-done-1',
+ timestamp: new Date().toISOString(),
+ parentId: null,
+ type: 'session.compaction_complete',
+ data: {
+ success: true,
+ preCompactionTokens: 100000,
+ postCompactionTokens: 5000,
+ messagesRemoved: 30,
+ tokensRemoved: 95000,
+ },
+ };
+
+ callbacks.onSessionEvent?.({ taskId: 'task-1', event: compactionStartEvent });
+ callbacks.onSessionEvent?.({ taskId: 'task-1', event: compactionCompleteEvent });
+
+ expect(sessionEventHandler).toHaveBeenCalledTimes(2);
+ expect(sessionEventHandler.mock.calls[0]![0].event.type).toBe('session.compaction_start');
+ expect(sessionEventHandler.mock.calls[1]![0].event.type).toBe('session.compaction_complete');
+ if (sessionEventHandler.mock.calls[1]![0].event.type === 'session.compaction_complete') {
+ expect(sessionEventHandler.mock.calls[1]![0].event.data.messagesRemoved).toBe(30);
+ }
+ });
+});
+
+describe('COMPACTION_THRESHOLD', () => {
+ it('should be 0.8 (80% context utilization)', () => {
+ expect(COMPACTION_THRESHOLD).toBe(0.8);
+ });
+
+ it('should be in the valid range 0.0–1.0', () => {
+ expect(COMPACTION_THRESHOLD).toBeGreaterThan(0);
+ expect(COMPACTION_THRESHOLD).toBeLessThan(1);
+ });
});
diff --git a/src/services/planner.ts b/src/services/planner.ts
index 1804bed..dd44ba5 100644
--- a/src/services/planner.ts
+++ b/src/services/planner.ts
@@ -179,13 +179,14 @@ export async function refineWBS(
refinementRequest: string,
onDelta?: (delta: string, fullText: string) => void,
skillOptions?: SkillOptions,
+ onSessionEvent?: (event: import('./copilot.js').SessionEvent) => void,
): Promise {
const result = await sendPromptSync(REFINE_SYSTEM_PROMPT, [
{
role: 'user',
content: `Current tasks:\n${JSON.stringify(currentTasks, null, 2)}\n\nRefinement request: ${refinementRequest}`,
},
- ], { onDelta, skillOptions });
+ ], { onDelta, skillOptions, onSessionEvent });
const jsonStr = extractJsonArray(result);
const tasks = JSON.parse(jsonStr) as Task[];