@mariozechner/pi-coding-agent 0.12.5 → 0.12.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -1
- package/README.md +86 -10
- package/dist/compaction.d.ts +51 -0
- package/dist/compaction.d.ts.map +1 -0
- package/dist/compaction.js +218 -0
- package/dist/compaction.js.map +1 -0
- package/dist/export-html.d.ts +5 -3
- package/dist/export-html.d.ts.map +1 -1
- package/dist/export-html.js +480 -1314
- package/dist/export-html.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +80 -3
- package/dist/main.js.map +1 -1
- package/dist/session-manager.d.ts +66 -1
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +175 -59
- package/dist/session-manager.js.map +1 -1
- package/dist/settings-manager.d.ts +15 -0
- package/dist/settings-manager.d.ts.map +1 -1
- package/dist/settings-manager.js +23 -0
- package/dist/settings-manager.js.map +1 -1
- package/dist/tools-manager.d.ts.map +1 -1
- package/dist/tools-manager.js +2 -2
- package/dist/tools-manager.js.map +1 -1
- package/dist/tui/compaction.d.ts +15 -0
- package/dist/tui/compaction.d.ts.map +1 -0
- package/dist/tui/compaction.js +42 -0
- package/dist/tui/compaction.js.map +1 -0
- package/dist/tui/footer.d.ts +2 -0
- package/dist/tui/footer.d.ts.map +1 -1
- package/dist/tui/footer.js +8 -3
- package/dist/tui/footer.js.map +1 -1
- package/dist/tui/tui-renderer.d.ts +8 -1
- package/dist/tui/tui-renderer.d.ts.map +1 -1
- package/dist/tui/tui-renderer.js +286 -28
- package/dist/tui/tui-renderer.js.map +1 -1
- package/package.json +4 -4
package/dist/tui/tui-renderer.js
CHANGED
|
@@ -3,13 +3,16 @@ import * as path from "node:path";
|
|
|
3
3
|
import { CombinedAutocompleteProvider, Container, Input, Loader, Markdown, ProcessTerminal, Spacer, Text, TruncatedText, TUI, visibleWidth, } from "@mariozechner/pi-tui";
|
|
4
4
|
import { exec } from "child_process";
|
|
5
5
|
import { getChangelogPath, parseChangelog } from "../changelog.js";
|
|
6
|
+
import { calculateContextTokens, compact, shouldCompact } from "../compaction.js";
|
|
6
7
|
import { APP_NAME, getDebugLogPath, getModelsPath, getOAuthPath } from "../config.js";
|
|
7
8
|
import { exportSessionToHtml } from "../export-html.js";
|
|
8
9
|
import { getApiKeyForModel, getAvailableModels, invalidateOAuthCache } from "../model-config.js";
|
|
9
10
|
import { listOAuthProviders, login, logout } from "../oauth/index.js";
|
|
11
|
+
import { getLatestCompactionEntry, loadSessionFromEntries, SUMMARY_PREFIX, SUMMARY_SUFFIX, } from "../session-manager.js";
|
|
10
12
|
import { expandSlashCommand, loadSlashCommands } from "../slash-commands.js";
|
|
11
13
|
import { getEditorTheme, getMarkdownTheme, onThemeChange, setTheme, theme } from "../theme/theme.js";
|
|
12
14
|
import { AssistantMessageComponent } from "./assistant-message.js";
|
|
15
|
+
import { CompactionComponent } from "./compaction.js";
|
|
13
16
|
import { CustomEditor } from "./custom-editor.js";
|
|
14
17
|
import { DynamicBorder } from "./dynamic-border.js";
|
|
15
18
|
import { FooterComponent } from "./footer.js";
|
|
@@ -86,6 +89,7 @@ export class TuiRenderer {
|
|
|
86
89
|
this.editorContainer = new Container(); // Container to hold editor or selector
|
|
87
90
|
this.editorContainer.addChild(this.editor); // Start with editor
|
|
88
91
|
this.footer = new FooterComponent(agent.state);
|
|
92
|
+
this.footer.setAutoCompactEnabled(this.settingsManager.getCompactionEnabled());
|
|
89
93
|
// Define slash commands
|
|
90
94
|
const thinkingCommand = {
|
|
91
95
|
name: "thinking",
|
|
@@ -131,6 +135,14 @@ export class TuiRenderer {
|
|
|
131
135
|
name: "clear",
|
|
132
136
|
description: "Clear context and start a fresh session",
|
|
133
137
|
};
|
|
138
|
+
const compactCommand = {
|
|
139
|
+
name: "compact",
|
|
140
|
+
description: "Manually compact the session context",
|
|
141
|
+
};
|
|
142
|
+
const autocompactCommand = {
|
|
143
|
+
name: "autocompact",
|
|
144
|
+
description: "Toggle automatic context compaction",
|
|
145
|
+
};
|
|
134
146
|
// Load file-based slash commands
|
|
135
147
|
this.fileCommands = loadSlashCommands();
|
|
136
148
|
// Convert file commands to SlashCommand format
|
|
@@ -151,6 +163,8 @@ export class TuiRenderer {
|
|
|
151
163
|
logoutCommand,
|
|
152
164
|
queueCommand,
|
|
153
165
|
clearCommand,
|
|
166
|
+
compactCommand,
|
|
167
|
+
autocompactCommand,
|
|
154
168
|
...fileSlashCommands,
|
|
155
169
|
], process.cwd(), fdPath);
|
|
156
170
|
this.editor.setAutocompleteProvider(autocompleteProvider);
|
|
@@ -322,6 +336,19 @@ export class TuiRenderer {
|
|
|
322
336
|
this.editor.setText("");
|
|
323
337
|
return;
|
|
324
338
|
}
|
|
339
|
+
// Check for /compact command
|
|
340
|
+
if (text === "/compact" || text.startsWith("/compact ")) {
|
|
341
|
+
const customInstructions = text.startsWith("/compact ") ? text.slice(9).trim() : undefined;
|
|
342
|
+
this.handleCompactCommand(customInstructions);
|
|
343
|
+
this.editor.setText("");
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
// Check for /autocompact command
|
|
347
|
+
if (text === "/autocompact") {
|
|
348
|
+
this.handleAutocompactCommand();
|
|
349
|
+
this.editor.setText("");
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
325
352
|
// Check for /debug command
|
|
326
353
|
if (text === "/debug") {
|
|
327
354
|
this.handleDebugCommand();
|
|
@@ -396,9 +423,39 @@ export class TuiRenderer {
|
|
|
396
423
|
if (this.sessionManager.shouldInitializeSession(this.agent.state.messages)) {
|
|
397
424
|
this.sessionManager.startSession(this.agent.state);
|
|
398
425
|
}
|
|
426
|
+
// Check for auto-compaction after assistant messages
|
|
427
|
+
if (event.message.role === "assistant") {
|
|
428
|
+
await this.checkAutoCompaction();
|
|
429
|
+
}
|
|
399
430
|
}
|
|
400
431
|
});
|
|
401
432
|
}
|
|
433
|
+
async checkAutoCompaction() {
|
|
434
|
+
const settings = this.settingsManager.getCompactionSettings();
|
|
435
|
+
if (!settings.enabled)
|
|
436
|
+
return;
|
|
437
|
+
// Get last non-aborted assistant message from agent state
|
|
438
|
+
const messages = this.agent.state.messages;
|
|
439
|
+
let lastAssistant = null;
|
|
440
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
441
|
+
const msg = messages[i];
|
|
442
|
+
if (msg.role === "assistant") {
|
|
443
|
+
const assistantMsg = msg;
|
|
444
|
+
if (assistantMsg.stopReason !== "aborted") {
|
|
445
|
+
lastAssistant = assistantMsg;
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (!lastAssistant)
|
|
451
|
+
return;
|
|
452
|
+
const contextTokens = calculateContextTokens(lastAssistant.usage);
|
|
453
|
+
const contextWindow = this.agent.state.model.contextWindow;
|
|
454
|
+
if (!shouldCompact(contextTokens, contextWindow, settings))
|
|
455
|
+
return;
|
|
456
|
+
// Trigger auto-compaction
|
|
457
|
+
await this.executeCompaction(undefined, true);
|
|
458
|
+
}
|
|
402
459
|
async handleEvent(event, state) {
|
|
403
460
|
if (!this.isInitialized) {
|
|
404
461
|
await this.init();
|
|
@@ -422,7 +479,9 @@ export class TuiRenderer {
|
|
|
422
479
|
if (event.message.role === "user") {
|
|
423
480
|
// Check if this is a queued message
|
|
424
481
|
const userMsg = event.message;
|
|
425
|
-
const textBlocks = userMsg.content
|
|
482
|
+
const textBlocks = typeof userMsg.content === "string"
|
|
483
|
+
? [{ type: "text", text: userMsg.content }]
|
|
484
|
+
: userMsg.content.filter((c) => c.type === "text");
|
|
426
485
|
const messageText = textBlocks.map((c) => c.text).join("");
|
|
427
486
|
const queuedIndex = this.queuedMessages.indexOf(messageText);
|
|
428
487
|
if (queuedIndex !== -1) {
|
|
@@ -550,7 +609,9 @@ export class TuiRenderer {
|
|
|
550
609
|
if (message.role === "user") {
|
|
551
610
|
const userMsg = message;
|
|
552
611
|
// Extract text content from content blocks
|
|
553
|
-
const textBlocks = userMsg.content
|
|
612
|
+
const textBlocks = typeof userMsg.content === "string"
|
|
613
|
+
? [{ type: "text", text: userMsg.content }]
|
|
614
|
+
: userMsg.content.filter((c) => c.type === "text");
|
|
554
615
|
const textContent = textBlocks.map((c) => c.text).join("");
|
|
555
616
|
if (textContent) {
|
|
556
617
|
const userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);
|
|
@@ -574,17 +635,30 @@ export class TuiRenderer {
|
|
|
574
635
|
this.footer.updateState(state);
|
|
575
636
|
// Update editor border color based on current thinking level
|
|
576
637
|
this.updateEditorBorderColor();
|
|
638
|
+
// Get compaction info if any
|
|
639
|
+
const compactionEntry = getLatestCompactionEntry(this.sessionManager.loadEntries());
|
|
577
640
|
// Render messages
|
|
578
641
|
for (let i = 0; i < state.messages.length; i++) {
|
|
579
642
|
const message = state.messages[i];
|
|
580
643
|
if (message.role === "user") {
|
|
581
644
|
const userMsg = message;
|
|
582
|
-
const textBlocks = userMsg.content
|
|
645
|
+
const textBlocks = typeof userMsg.content === "string"
|
|
646
|
+
? [{ type: "text", text: userMsg.content }]
|
|
647
|
+
: userMsg.content.filter((c) => c.type === "text");
|
|
583
648
|
const textContent = textBlocks.map((c) => c.text).join("");
|
|
584
649
|
if (textContent) {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
650
|
+
// Check if this is a compaction summary message
|
|
651
|
+
if (textContent.startsWith(SUMMARY_PREFIX) && compactionEntry) {
|
|
652
|
+
const summary = textContent.slice(SUMMARY_PREFIX.length, -SUMMARY_SUFFIX.length);
|
|
653
|
+
const component = new CompactionComponent(compactionEntry.tokensBefore, summary);
|
|
654
|
+
component.setExpanded(this.toolOutputExpanded);
|
|
655
|
+
this.chatContainer.addChild(component);
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
const userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);
|
|
659
|
+
this.chatContainer.addChild(userComponent);
|
|
660
|
+
this.isFirstUserMessage = false;
|
|
661
|
+
}
|
|
588
662
|
}
|
|
589
663
|
}
|
|
590
664
|
else if (message.role === "assistant") {
|
|
@@ -639,6 +713,61 @@ export class TuiRenderer {
|
|
|
639
713
|
};
|
|
640
714
|
});
|
|
641
715
|
}
|
|
716
|
+
rebuildChatFromMessages() {
|
|
717
|
+
// Reset state and re-render messages from agent state
|
|
718
|
+
this.isFirstUserMessage = true;
|
|
719
|
+
this.pendingTools.clear();
|
|
720
|
+
// Get compaction info if any
|
|
721
|
+
const compactionEntry = getLatestCompactionEntry(this.sessionManager.loadEntries());
|
|
722
|
+
for (const message of this.agent.state.messages) {
|
|
723
|
+
if (message.role === "user") {
|
|
724
|
+
const userMsg = message;
|
|
725
|
+
const textBlocks = typeof userMsg.content === "string"
|
|
726
|
+
? [{ type: "text", text: userMsg.content }]
|
|
727
|
+
: userMsg.content.filter((c) => c.type === "text");
|
|
728
|
+
const textContent = textBlocks.map((c) => c.text).join("");
|
|
729
|
+
if (textContent) {
|
|
730
|
+
// Check if this is a compaction summary message
|
|
731
|
+
if (textContent.startsWith(SUMMARY_PREFIX) && compactionEntry) {
|
|
732
|
+
const summary = textContent.slice(SUMMARY_PREFIX.length, -SUMMARY_SUFFIX.length);
|
|
733
|
+
const component = new CompactionComponent(compactionEntry.tokensBefore, summary);
|
|
734
|
+
component.setExpanded(this.toolOutputExpanded);
|
|
735
|
+
this.chatContainer.addChild(component);
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
const userComponent = new UserMessageComponent(textContent, this.isFirstUserMessage);
|
|
739
|
+
this.chatContainer.addChild(userComponent);
|
|
740
|
+
this.isFirstUserMessage = false;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
else if (message.role === "assistant") {
|
|
745
|
+
const assistantMsg = message;
|
|
746
|
+
const assistantComponent = new AssistantMessageComponent(assistantMsg);
|
|
747
|
+
this.chatContainer.addChild(assistantComponent);
|
|
748
|
+
for (const content of assistantMsg.content) {
|
|
749
|
+
if (content.type === "toolCall") {
|
|
750
|
+
const component = new ToolExecutionComponent(content.name, content.arguments);
|
|
751
|
+
this.chatContainer.addChild(component);
|
|
752
|
+
this.pendingTools.set(content.id, component);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
else if (message.role === "toolResult") {
|
|
757
|
+
const component = this.pendingTools.get(message.toolCallId);
|
|
758
|
+
if (component) {
|
|
759
|
+
component.updateResult({
|
|
760
|
+
content: message.content,
|
|
761
|
+
details: message.details,
|
|
762
|
+
isError: message.isError,
|
|
763
|
+
});
|
|
764
|
+
this.pendingTools.delete(message.toolCallId);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
this.pendingTools.clear();
|
|
769
|
+
this.ui.requestRender();
|
|
770
|
+
}
|
|
642
771
|
handleCtrlC() {
|
|
643
772
|
// Handle Ctrl+C double-press logic
|
|
644
773
|
const now = Date.now();
|
|
@@ -771,11 +900,14 @@ export class TuiRenderer {
|
|
|
771
900
|
}
|
|
772
901
|
toggleToolOutputExpansion() {
|
|
773
902
|
this.toolOutputExpanded = !this.toolOutputExpanded;
|
|
774
|
-
// Update all tool execution components
|
|
903
|
+
// Update all tool execution and compaction components
|
|
775
904
|
for (const child of this.chatContainer.children) {
|
|
776
905
|
if (child instanceof ToolExecutionComponent) {
|
|
777
906
|
child.setExpanded(this.toolOutputExpanded);
|
|
778
907
|
}
|
|
908
|
+
else if (child instanceof CompactionComponent) {
|
|
909
|
+
child.setExpanded(this.toolOutputExpanded);
|
|
910
|
+
}
|
|
779
911
|
}
|
|
780
912
|
this.ui.requestRender();
|
|
781
913
|
}
|
|
@@ -795,6 +927,14 @@ export class TuiRenderer {
|
|
|
795
927
|
this.chatContainer.addChild(new Text(theme.fg("warning", `Warning: ${warningMessage}`), 1, 0));
|
|
796
928
|
this.ui.requestRender();
|
|
797
929
|
}
|
|
930
|
+
showSuccess(message, detail) {
|
|
931
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
932
|
+
const text = detail
|
|
933
|
+
? `${theme.fg("success", message)}\n${theme.fg("muted", detail)}`
|
|
934
|
+
: theme.fg("success", message);
|
|
935
|
+
this.chatContainer.addChild(new Text(text, 1, 1));
|
|
936
|
+
this.ui.requestRender();
|
|
937
|
+
}
|
|
798
938
|
showThinkingSelector() {
|
|
799
939
|
// Create thinking selector with current level
|
|
800
940
|
this.thinkingSelector = new ThinkingSelectorComponent(this.agent.state.thinkingLevel, (level) => {
|
|
@@ -945,17 +1085,30 @@ export class TuiRenderer {
|
|
|
945
1085
|
this.ui.setFocus(this.editor);
|
|
946
1086
|
}
|
|
947
1087
|
showUserMessageSelector() {
|
|
948
|
-
//
|
|
1088
|
+
// Read from session file directly to see ALL historical user messages
|
|
1089
|
+
// (including those before compaction events)
|
|
1090
|
+
const entries = this.sessionManager.loadEntries();
|
|
949
1091
|
const userMessages = [];
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1092
|
+
const getUserMessageText = (content) => {
|
|
1093
|
+
if (typeof content === "string")
|
|
1094
|
+
return content;
|
|
1095
|
+
if (Array.isArray(content)) {
|
|
1096
|
+
return content
|
|
1097
|
+
.filter((c) => c.type === "text")
|
|
1098
|
+
.map((c) => c.text)
|
|
1099
|
+
.join("");
|
|
1100
|
+
}
|
|
1101
|
+
return "";
|
|
1102
|
+
};
|
|
1103
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1104
|
+
const entry = entries[i];
|
|
1105
|
+
if (entry.type !== "message")
|
|
1106
|
+
continue;
|
|
1107
|
+
if (entry.message.role !== "user")
|
|
1108
|
+
continue;
|
|
1109
|
+
const textContent = getUserMessageText(entry.message.content);
|
|
1110
|
+
if (textContent) {
|
|
1111
|
+
userMessages.push({ index: i, text: textContent });
|
|
959
1112
|
}
|
|
960
1113
|
}
|
|
961
1114
|
// Don't show selector if there are no messages or only one message
|
|
@@ -966,26 +1119,28 @@ export class TuiRenderer {
|
|
|
966
1119
|
return;
|
|
967
1120
|
}
|
|
968
1121
|
// Create user message selector
|
|
969
|
-
this.userMessageSelector = new UserMessageSelectorComponent(userMessages, (
|
|
1122
|
+
this.userMessageSelector = new UserMessageSelectorComponent(userMessages, (entryIndex) => {
|
|
970
1123
|
// Get the selected user message text to put in the editor
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
const
|
|
1124
|
+
const selectedEntry = entries[entryIndex];
|
|
1125
|
+
if (selectedEntry.type !== "message")
|
|
1126
|
+
return;
|
|
1127
|
+
if (selectedEntry.message.role !== "user")
|
|
1128
|
+
return;
|
|
1129
|
+
const selectedText = getUserMessageText(selectedEntry.message.content);
|
|
1130
|
+
// Create a branched session by copying entries up to (but not including) the selected entry
|
|
1131
|
+
const newSessionFile = this.sessionManager.createBranchedSessionFromEntries(entries, entryIndex);
|
|
977
1132
|
// Set the new session file as active
|
|
978
1133
|
this.sessionManager.setSessionFile(newSessionFile);
|
|
979
|
-
//
|
|
980
|
-
const
|
|
981
|
-
this.agent.replaceMessages(
|
|
1134
|
+
// Reload the session
|
|
1135
|
+
const loaded = loadSessionFromEntries(this.sessionManager.loadEntries());
|
|
1136
|
+
this.agent.replaceMessages(loaded.messages);
|
|
982
1137
|
// Clear and re-render the chat
|
|
983
1138
|
this.chatContainer.clear();
|
|
984
1139
|
this.isFirstUserMessage = true;
|
|
985
1140
|
this.renderInitialMessages(this.agent.state);
|
|
986
1141
|
// Show confirmation message
|
|
987
1142
|
this.chatContainer.addChild(new Spacer(1));
|
|
988
|
-
this.chatContainer.addChild(new Text(theme.fg("dim",
|
|
1143
|
+
this.chatContainer.addChild(new Text(theme.fg("dim", "Branched to new session"), 1, 0));
|
|
989
1144
|
// Put the selected message in the editor
|
|
990
1145
|
this.editor.setText(selectedText);
|
|
991
1146
|
// Hide selector and show editor again
|
|
@@ -1260,6 +1415,109 @@ export class TuiRenderer {
|
|
|
1260
1415
|
this.chatContainer.addChild(new Text(theme.fg("accent", "✓ Debug log written") + "\n" + theme.fg("muted", debugLogPath), 1, 1));
|
|
1261
1416
|
this.ui.requestRender();
|
|
1262
1417
|
}
|
|
1418
|
+
compactionAbortController = null;
|
|
1419
|
+
/**
|
|
1420
|
+
* Shared logic to execute context compaction.
|
|
1421
|
+
* Handles aborting agent, showing loader, performing compaction, updating session/UI.
|
|
1422
|
+
*/
|
|
1423
|
+
async executeCompaction(customInstructions, isAuto = false) {
|
|
1424
|
+
// Unsubscribe first to prevent processing events during compaction
|
|
1425
|
+
this.unsubscribe?.();
|
|
1426
|
+
// Abort and wait for completion
|
|
1427
|
+
this.agent.abort();
|
|
1428
|
+
await this.agent.waitForIdle();
|
|
1429
|
+
// Stop loading animation
|
|
1430
|
+
if (this.loadingAnimation) {
|
|
1431
|
+
this.loadingAnimation.stop();
|
|
1432
|
+
this.loadingAnimation = null;
|
|
1433
|
+
}
|
|
1434
|
+
this.statusContainer.clear();
|
|
1435
|
+
// Create abort controller for compaction
|
|
1436
|
+
this.compactionAbortController = new AbortController();
|
|
1437
|
+
// Set up escape handler during compaction
|
|
1438
|
+
const originalOnEscape = this.editor.onEscape;
|
|
1439
|
+
this.editor.onEscape = () => {
|
|
1440
|
+
if (this.compactionAbortController) {
|
|
1441
|
+
this.compactionAbortController.abort();
|
|
1442
|
+
}
|
|
1443
|
+
};
|
|
1444
|
+
// Show compacting status with loader
|
|
1445
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
1446
|
+
const label = isAuto ? "Auto-compacting context... (esc to cancel)" : "Compacting context... (esc to cancel)";
|
|
1447
|
+
const compactingLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), label);
|
|
1448
|
+
this.statusContainer.addChild(compactingLoader);
|
|
1449
|
+
this.ui.requestRender();
|
|
1450
|
+
try {
|
|
1451
|
+
// Get API key for current model
|
|
1452
|
+
const apiKey = await getApiKeyForModel(this.agent.state.model);
|
|
1453
|
+
if (!apiKey) {
|
|
1454
|
+
throw new Error(`No API key for ${this.agent.state.model.provider}`);
|
|
1455
|
+
}
|
|
1456
|
+
// Perform compaction with abort signal
|
|
1457
|
+
const entries = this.sessionManager.loadEntries();
|
|
1458
|
+
const settings = this.settingsManager.getCompactionSettings();
|
|
1459
|
+
const compactionEntry = await compact(entries, this.agent.state.model, settings, apiKey, this.compactionAbortController.signal, customInstructions);
|
|
1460
|
+
// Check if aborted after compact returned
|
|
1461
|
+
if (this.compactionAbortController.signal.aborted) {
|
|
1462
|
+
throw new Error("Compaction cancelled");
|
|
1463
|
+
}
|
|
1464
|
+
// Save compaction to session
|
|
1465
|
+
this.sessionManager.saveCompaction(compactionEntry);
|
|
1466
|
+
// Reload session
|
|
1467
|
+
const loaded = loadSessionFromEntries(this.sessionManager.loadEntries());
|
|
1468
|
+
this.agent.replaceMessages(loaded.messages);
|
|
1469
|
+
// Rebuild UI
|
|
1470
|
+
this.chatContainer.clear();
|
|
1471
|
+
this.rebuildChatFromMessages();
|
|
1472
|
+
// Add compaction component at current position so user can see/expand the summary
|
|
1473
|
+
const compactionComponent = new CompactionComponent(compactionEntry.tokensBefore, compactionEntry.summary);
|
|
1474
|
+
compactionComponent.setExpanded(this.toolOutputExpanded);
|
|
1475
|
+
this.chatContainer.addChild(compactionComponent);
|
|
1476
|
+
// Update footer with new state (fixes context % display)
|
|
1477
|
+
this.footer.updateState(this.agent.state);
|
|
1478
|
+
// Show success message
|
|
1479
|
+
const successTitle = isAuto ? "✓ Context auto-compacted" : "✓ Context compacted";
|
|
1480
|
+
this.showSuccess(successTitle, `Reduced from ${compactionEntry.tokensBefore.toLocaleString()} tokens`);
|
|
1481
|
+
}
|
|
1482
|
+
catch (error) {
|
|
1483
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1484
|
+
if (message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError")) {
|
|
1485
|
+
this.showError("Compaction cancelled");
|
|
1486
|
+
}
|
|
1487
|
+
else {
|
|
1488
|
+
this.showError(`Compaction failed: ${message}`);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
finally {
|
|
1492
|
+
// Clean up
|
|
1493
|
+
compactingLoader.stop();
|
|
1494
|
+
this.statusContainer.clear();
|
|
1495
|
+
this.compactionAbortController = null;
|
|
1496
|
+
this.editor.onEscape = originalOnEscape;
|
|
1497
|
+
}
|
|
1498
|
+
// Resubscribe to agent
|
|
1499
|
+
this.subscribeToAgent();
|
|
1500
|
+
}
|
|
1501
|
+
async handleCompactCommand(customInstructions) {
|
|
1502
|
+
// Check if there are any messages to compact
|
|
1503
|
+
const entries = this.sessionManager.loadEntries();
|
|
1504
|
+
const messageCount = entries.filter((e) => e.type === "message").length;
|
|
1505
|
+
if (messageCount < 2) {
|
|
1506
|
+
this.showWarning("Nothing to compact (no messages yet)");
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
await this.executeCompaction(customInstructions, false);
|
|
1510
|
+
}
|
|
1511
|
+
handleAutocompactCommand() {
|
|
1512
|
+
const currentEnabled = this.settingsManager.getCompactionEnabled();
|
|
1513
|
+
const newState = !currentEnabled;
|
|
1514
|
+
this.settingsManager.setCompactionEnabled(newState);
|
|
1515
|
+
this.footer.setAutoCompactEnabled(newState);
|
|
1516
|
+
// Show brief notification (same style as thinking level toggle)
|
|
1517
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
1518
|
+
this.chatContainer.addChild(new Text(theme.fg("dim", `Auto-compaction: ${newState ? "on" : "off"}`), 1, 0));
|
|
1519
|
+
this.ui.requestRender();
|
|
1520
|
+
}
|
|
1263
1521
|
updatePendingMessagesDisplay() {
|
|
1264
1522
|
this.pendingMessagesContainer.clear();
|
|
1265
1523
|
if (this.queuedMessages.length > 0) {
|