@mariozechner/pi-coding-agent 0.13.2 → 0.14.1
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 +21 -1
- package/README.md +62 -0
- package/dist/compaction.d.ts.map +1 -1
- package/dist/compaction.js +5 -2
- package/dist/compaction.js.map +1 -1
- package/dist/export-html.d.ts.map +1 -1
- package/dist/export-html.js +52 -25
- package/dist/export-html.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +120 -9
- package/dist/main.js.map +1 -1
- package/dist/messages.d.ts +43 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +71 -0
- package/dist/messages.js.map +1 -0
- package/dist/model-config.d.ts.map +1 -1
- package/dist/model-config.js +9 -0
- package/dist/model-config.js.map +1 -1
- package/dist/settings-manager.d.ts +6 -3
- package/dist/settings-manager.d.ts.map +1 -1
- package/dist/settings-manager.js +7 -0
- package/dist/settings-manager.js.map +1 -1
- package/dist/shell.d.ts +16 -0
- package/dist/shell.d.ts.map +1 -0
- package/dist/shell.js +108 -0
- package/dist/shell.js.map +1 -0
- package/dist/theme/dark.json +4 -1
- package/dist/theme/light.json +4 -1
- package/dist/theme/theme-schema.json +28 -0
- package/dist/theme/theme.d.ts +3 -2
- package/dist/theme/theme.d.ts.map +1 -1
- package/dist/theme/theme.js +32 -3
- package/dist/theme/theme.js.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +3 -107
- package/dist/tools/bash.js.map +1 -1
- package/dist/tui/bash-execution.d.ts +33 -0
- package/dist/tui/bash-execution.d.ts.map +1 -0
- package/dist/tui/bash-execution.js +134 -0
- package/dist/tui/bash-execution.js.map +1 -0
- package/dist/tui/tui-renderer.d.ts +7 -1
- package/dist/tui/tui-renderer.d.ts.map +1 -1
- package/dist/tui/tui-renderer.js +225 -10
- package/dist/tui/tui-renderer.js.map +1 -1
- package/package.json +4 -4
- package/dist/fuzzy.test.d.ts +0 -2
- package/dist/fuzzy.test.d.ts.map +0 -1
- package/dist/fuzzy.test.js +0 -76
- package/dist/fuzzy.test.js.map +0 -1
package/dist/tui/tui-renderer.js
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import * as fs from "node:fs";
|
|
3
|
+
import { createWriteStream } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
2
5
|
import * as path from "node:path";
|
|
6
|
+
import { join } from "node:path";
|
|
3
7
|
import { CombinedAutocompleteProvider, Container, Input, Loader, Markdown, ProcessTerminal, Spacer, Text, TruncatedText, TUI, visibleWidth, } from "@mariozechner/pi-tui";
|
|
4
|
-
import { exec } from "child_process";
|
|
8
|
+
import { exec, spawn } from "child_process";
|
|
9
|
+
import stripAnsi from "strip-ansi";
|
|
5
10
|
import { getChangelogPath, parseChangelog } from "../changelog.js";
|
|
6
11
|
import { copyToClipboard } from "../clipboard.js";
|
|
7
12
|
import { calculateContextTokens, compact, shouldCompact } from "../compaction.js";
|
|
8
13
|
import { APP_NAME, getDebugLogPath, getModelsPath, getOAuthPath } from "../config.js";
|
|
9
14
|
import { exportSessionToHtml } from "../export-html.js";
|
|
15
|
+
import { isBashExecutionMessage } from "../messages.js";
|
|
10
16
|
import { getApiKeyForModel, getAvailableModels, invalidateOAuthCache } from "../model-config.js";
|
|
11
17
|
import { listOAuthProviders, login, logout } from "../oauth/index.js";
|
|
12
18
|
import { getLatestCompactionEntry, loadSessionFromEntries, SUMMARY_PREFIX, SUMMARY_SUFFIX, } from "../session-manager.js";
|
|
19
|
+
import { getShellConfig, killProcessTree } from "../shell.js";
|
|
13
20
|
import { expandSlashCommand, loadSlashCommands } from "../slash-commands.js";
|
|
14
21
|
import { getEditorTheme, getMarkdownTheme, onThemeChange, setTheme, theme } from "../theme/theme.js";
|
|
22
|
+
import { DEFAULT_MAX_BYTES, truncateTail } from "../tools/truncate.js";
|
|
15
23
|
import { AssistantMessageComponent } from "./assistant-message.js";
|
|
24
|
+
import { BashExecutionComponent } from "./bash-execution.js";
|
|
16
25
|
import { CompactionComponent } from "./compaction.js";
|
|
17
26
|
import { CustomEditor } from "./custom-editor.js";
|
|
18
27
|
import { DynamicBorder } from "./dynamic-border.js";
|
|
@@ -47,6 +56,7 @@ export class TuiRenderer {
|
|
|
47
56
|
lastSigintTime = 0;
|
|
48
57
|
lastEscapeTime = 0;
|
|
49
58
|
changelogMarkdown = null;
|
|
59
|
+
collapseChangelog = false;
|
|
50
60
|
// Message queueing
|
|
51
61
|
queuedMessages = [];
|
|
52
62
|
// Streaming message tracking
|
|
@@ -79,12 +89,19 @@ export class TuiRenderer {
|
|
|
79
89
|
unsubscribe;
|
|
80
90
|
// File-based slash commands
|
|
81
91
|
fileCommands = [];
|
|
82
|
-
|
|
92
|
+
// Track if editor is in bash mode (text starts with !)
|
|
93
|
+
isBashMode = false;
|
|
94
|
+
// Track running bash command process for cancellation
|
|
95
|
+
bashProcess = null;
|
|
96
|
+
// Track current bash execution component
|
|
97
|
+
bashComponent = null;
|
|
98
|
+
constructor(agent, sessionManager, settingsManager, version, changelogMarkdown = null, collapseChangelog = false, scopedModels = [], fdPath = null) {
|
|
83
99
|
this.agent = agent;
|
|
84
100
|
this.sessionManager = sessionManager;
|
|
85
101
|
this.settingsManager = settingsManager;
|
|
86
102
|
this.version = version;
|
|
87
103
|
this.changelogMarkdown = changelogMarkdown;
|
|
104
|
+
this.collapseChangelog = collapseChangelog;
|
|
88
105
|
this.scopedModels = scopedModels;
|
|
89
106
|
this.ui = new TUI(new ProcessTerminal());
|
|
90
107
|
this.chatContainer = new Container();
|
|
@@ -218,6 +235,9 @@ export class TuiRenderer {
|
|
|
218
235
|
theme.fg("dim", "/") +
|
|
219
236
|
theme.fg("muted", " for commands") +
|
|
220
237
|
"\n" +
|
|
238
|
+
theme.fg("dim", "!") +
|
|
239
|
+
theme.fg("muted", " to run bash") +
|
|
240
|
+
"\n" +
|
|
221
241
|
theme.fg("dim", "drop files") +
|
|
222
242
|
theme.fg("muted", " to attach");
|
|
223
243
|
const header = new Text(logo + "\n" + instructions, 1, 0);
|
|
@@ -228,10 +248,19 @@ export class TuiRenderer {
|
|
|
228
248
|
// Add changelog if provided
|
|
229
249
|
if (this.changelogMarkdown) {
|
|
230
250
|
this.ui.addChild(new DynamicBorder());
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
251
|
+
if (this.collapseChangelog) {
|
|
252
|
+
// Show condensed version with hint to use /changelog
|
|
253
|
+
const versionMatch = this.changelogMarkdown.match(/##\s+\[?(\d+\.\d+\.\d+)\]?/);
|
|
254
|
+
const latestVersion = versionMatch ? versionMatch[1] : this.version;
|
|
255
|
+
const condensedText = `Updated to v${latestVersion}. Use ${theme.bold("/changelog")} to view full changelog.`;
|
|
256
|
+
this.ui.addChild(new Text(condensedText, 1, 0));
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
this.ui.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0));
|
|
260
|
+
this.ui.addChild(new Spacer(1));
|
|
261
|
+
this.ui.addChild(new Markdown(this.changelogMarkdown.trim(), 1, 0, getMarkdownTheme()));
|
|
262
|
+
this.ui.addChild(new Spacer(1));
|
|
263
|
+
}
|
|
235
264
|
this.ui.addChild(new DynamicBorder());
|
|
236
265
|
}
|
|
237
266
|
this.ui.addChild(this.chatContainer);
|
|
@@ -261,6 +290,19 @@ export class TuiRenderer {
|
|
|
261
290
|
// Abort
|
|
262
291
|
this.agent.abort();
|
|
263
292
|
}
|
|
293
|
+
else if (this.bashProcess) {
|
|
294
|
+
// Kill running bash command
|
|
295
|
+
if (this.bashProcess.pid) {
|
|
296
|
+
killProcessTree(this.bashProcess.pid);
|
|
297
|
+
}
|
|
298
|
+
this.bashProcess = null;
|
|
299
|
+
}
|
|
300
|
+
else if (this.isBashMode) {
|
|
301
|
+
// Cancel bash mode and clear editor
|
|
302
|
+
this.editor.setText("");
|
|
303
|
+
this.isBashMode = false;
|
|
304
|
+
this.updateEditorBorderColor();
|
|
305
|
+
}
|
|
264
306
|
else if (!this.editor.getText().trim()) {
|
|
265
307
|
// Double-escape with empty editor triggers /branch
|
|
266
308
|
const now = Date.now();
|
|
@@ -288,6 +330,14 @@ export class TuiRenderer {
|
|
|
288
330
|
this.editor.onCtrlT = () => {
|
|
289
331
|
this.toggleThinkingBlockVisibility();
|
|
290
332
|
};
|
|
333
|
+
// Handle editor text changes for bash mode detection
|
|
334
|
+
this.editor.onChange = (text) => {
|
|
335
|
+
const wasBashMode = this.isBashMode;
|
|
336
|
+
this.isBashMode = text.trimStart().startsWith("!");
|
|
337
|
+
if (wasBashMode !== this.isBashMode) {
|
|
338
|
+
this.updateEditorBorderColor();
|
|
339
|
+
}
|
|
340
|
+
};
|
|
291
341
|
// Handle editor submission
|
|
292
342
|
this.editor.onSubmit = async (text) => {
|
|
293
343
|
text = text.trim();
|
|
@@ -392,6 +442,26 @@ export class TuiRenderer {
|
|
|
392
442
|
this.editor.setText("");
|
|
393
443
|
return;
|
|
394
444
|
}
|
|
445
|
+
// Check for bash command (!<command>)
|
|
446
|
+
if (text.startsWith("!")) {
|
|
447
|
+
const command = text.slice(1).trim();
|
|
448
|
+
if (command) {
|
|
449
|
+
// Block if bash already running
|
|
450
|
+
if (this.bashProcess) {
|
|
451
|
+
this.showWarning("A bash command is already running. Press Esc to cancel it first.");
|
|
452
|
+
// Restore text since editor clears on submit
|
|
453
|
+
this.editor.setText(text);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
// Add to history for up/down arrow navigation
|
|
457
|
+
this.editor.addToHistory(text);
|
|
458
|
+
this.handleBashCommand(command);
|
|
459
|
+
// Reset bash mode since editor is now empty
|
|
460
|
+
this.isBashMode = false;
|
|
461
|
+
this.updateEditorBorderColor();
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
395
465
|
// Check for file-based slash commands
|
|
396
466
|
text = expandSlashCommand(text, this.fileCommands);
|
|
397
467
|
// Normal message submission - validate model and API key first
|
|
@@ -647,6 +717,17 @@ export class TuiRenderer {
|
|
|
647
717
|
}
|
|
648
718
|
}
|
|
649
719
|
addMessageToChat(message) {
|
|
720
|
+
// Handle bash execution messages
|
|
721
|
+
if (isBashExecutionMessage(message)) {
|
|
722
|
+
const bashMsg = message;
|
|
723
|
+
const component = new BashExecutionComponent(bashMsg.command, this.ui);
|
|
724
|
+
if (bashMsg.output) {
|
|
725
|
+
component.appendOutput(bashMsg.output);
|
|
726
|
+
}
|
|
727
|
+
component.setComplete(bashMsg.exitCode, bashMsg.cancelled, bashMsg.truncated ? { truncated: true } : undefined, bashMsg.fullOutputPath);
|
|
728
|
+
this.chatContainer.addChild(component);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
650
731
|
if (message.role === "user") {
|
|
651
732
|
const userMsg = message;
|
|
652
733
|
// Extract text content from content blocks
|
|
@@ -681,6 +762,11 @@ export class TuiRenderer {
|
|
|
681
762
|
// Render messages
|
|
682
763
|
for (let i = 0; i < state.messages.length; i++) {
|
|
683
764
|
const message = state.messages[i];
|
|
765
|
+
// Handle bash execution messages
|
|
766
|
+
if (isBashExecutionMessage(message)) {
|
|
767
|
+
this.addMessageToChat(message);
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
684
770
|
if (message.role === "user") {
|
|
685
771
|
const userMsg = message;
|
|
686
772
|
const textBlocks = typeof userMsg.content === "string"
|
|
@@ -774,6 +860,11 @@ export class TuiRenderer {
|
|
|
774
860
|
// Get compaction info if any
|
|
775
861
|
const compactionEntry = getLatestCompactionEntry(this.sessionManager.loadEntries());
|
|
776
862
|
for (const message of this.agent.state.messages) {
|
|
863
|
+
// Handle bash execution messages
|
|
864
|
+
if (isBashExecutionMessage(message)) {
|
|
865
|
+
this.addMessageToChat(message);
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
777
868
|
if (message.role === "user") {
|
|
778
869
|
const userMsg = message;
|
|
779
870
|
const textBlocks = typeof userMsg.content === "string"
|
|
@@ -838,8 +929,13 @@ export class TuiRenderer {
|
|
|
838
929
|
}
|
|
839
930
|
}
|
|
840
931
|
updateEditorBorderColor() {
|
|
841
|
-
|
|
842
|
-
|
|
932
|
+
if (this.isBashMode) {
|
|
933
|
+
this.editor.borderColor = theme.getBashModeBorderColor();
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
const level = this.agent.state.thinkingLevel || "off";
|
|
937
|
+
this.editor.borderColor = theme.getThinkingBorderColor(level);
|
|
938
|
+
}
|
|
843
939
|
this.ui.requestRender();
|
|
844
940
|
}
|
|
845
941
|
cycleThinkingLevel() {
|
|
@@ -850,7 +946,12 @@ export class TuiRenderer {
|
|
|
850
946
|
this.ui.requestRender();
|
|
851
947
|
return;
|
|
852
948
|
}
|
|
853
|
-
|
|
949
|
+
// xhigh is only available for codex-max models
|
|
950
|
+
const modelId = this.agent.state.model?.id || "";
|
|
951
|
+
const supportsXhigh = modelId.includes("codex-max");
|
|
952
|
+
const levels = supportsXhigh
|
|
953
|
+
? ["off", "minimal", "low", "medium", "high", "xhigh"]
|
|
954
|
+
: ["off", "minimal", "low", "medium", "high"];
|
|
854
955
|
const currentLevel = this.agent.state.thinkingLevel || "off";
|
|
855
956
|
const currentIndex = levels.indexOf(currentLevel);
|
|
856
957
|
const nextIndex = (currentIndex + 1) % levels.length;
|
|
@@ -954,7 +1055,7 @@ export class TuiRenderer {
|
|
|
954
1055
|
}
|
|
955
1056
|
toggleToolOutputExpansion() {
|
|
956
1057
|
this.toolOutputExpanded = !this.toolOutputExpanded;
|
|
957
|
-
// Update all tool execution and
|
|
1058
|
+
// Update all tool execution, compaction, and bash execution components
|
|
958
1059
|
for (const child of this.chatContainer.children) {
|
|
959
1060
|
if (child instanceof ToolExecutionComponent) {
|
|
960
1061
|
child.setExpanded(this.toolOutputExpanded);
|
|
@@ -962,6 +1063,9 @@ export class TuiRenderer {
|
|
|
962
1063
|
else if (child instanceof CompactionComponent) {
|
|
963
1064
|
child.setExpanded(this.toolOutputExpanded);
|
|
964
1065
|
}
|
|
1066
|
+
else if (child instanceof BashExecutionComponent) {
|
|
1067
|
+
child.setExpanded(this.toolOutputExpanded);
|
|
1068
|
+
}
|
|
965
1069
|
}
|
|
966
1070
|
this.ui.requestRender();
|
|
967
1071
|
}
|
|
@@ -1594,6 +1698,117 @@ export class TuiRenderer {
|
|
|
1594
1698
|
this.chatContainer.addChild(new Text(theme.fg("accent", "✓ Debug log written") + "\n" + theme.fg("muted", debugLogPath), 1, 1));
|
|
1595
1699
|
this.ui.requestRender();
|
|
1596
1700
|
}
|
|
1701
|
+
async handleBashCommand(command) {
|
|
1702
|
+
// Create component and add to chat
|
|
1703
|
+
this.bashComponent = new BashExecutionComponent(command, this.ui);
|
|
1704
|
+
this.chatContainer.addChild(this.bashComponent);
|
|
1705
|
+
this.ui.requestRender();
|
|
1706
|
+
try {
|
|
1707
|
+
const result = await this.executeBashCommand(command, (chunk) => {
|
|
1708
|
+
if (this.bashComponent) {
|
|
1709
|
+
this.bashComponent.appendOutput(chunk);
|
|
1710
|
+
this.ui.requestRender();
|
|
1711
|
+
}
|
|
1712
|
+
});
|
|
1713
|
+
if (this.bashComponent) {
|
|
1714
|
+
this.bashComponent.setComplete(result.exitCode, result.cancelled, result.truncationResult, result.fullOutputPath);
|
|
1715
|
+
// Create and save message (even if cancelled, for consistency with LLM aborts)
|
|
1716
|
+
const bashMessage = {
|
|
1717
|
+
role: "bashExecution",
|
|
1718
|
+
command,
|
|
1719
|
+
output: result.truncationResult?.content || this.bashComponent.getOutput(),
|
|
1720
|
+
exitCode: result.exitCode,
|
|
1721
|
+
cancelled: result.cancelled,
|
|
1722
|
+
truncated: result.truncationResult?.truncated || false,
|
|
1723
|
+
fullOutputPath: result.fullOutputPath,
|
|
1724
|
+
timestamp: Date.now(),
|
|
1725
|
+
};
|
|
1726
|
+
// Add to agent state
|
|
1727
|
+
this.agent.appendMessage(bashMessage);
|
|
1728
|
+
// Save to session
|
|
1729
|
+
this.sessionManager.saveMessage(bashMessage);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
catch (error) {
|
|
1733
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1734
|
+
if (this.bashComponent) {
|
|
1735
|
+
this.bashComponent.setComplete(null, false);
|
|
1736
|
+
}
|
|
1737
|
+
this.showError(`Bash command failed: ${errorMessage}`);
|
|
1738
|
+
}
|
|
1739
|
+
this.bashComponent = null;
|
|
1740
|
+
this.ui.requestRender();
|
|
1741
|
+
}
|
|
1742
|
+
executeBashCommand(command, onChunk) {
|
|
1743
|
+
return new Promise((resolve, reject) => {
|
|
1744
|
+
const { shell, args } = getShellConfig();
|
|
1745
|
+
const child = spawn(shell, [...args, command], {
|
|
1746
|
+
detached: true,
|
|
1747
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1748
|
+
});
|
|
1749
|
+
this.bashProcess = child;
|
|
1750
|
+
// Track output for truncation
|
|
1751
|
+
const chunks = [];
|
|
1752
|
+
let chunksBytes = 0;
|
|
1753
|
+
const maxChunksBytes = DEFAULT_MAX_BYTES * 2;
|
|
1754
|
+
// Temp file for large output
|
|
1755
|
+
let tempFilePath;
|
|
1756
|
+
let tempFileStream;
|
|
1757
|
+
let totalBytes = 0;
|
|
1758
|
+
const handleData = (data) => {
|
|
1759
|
+
totalBytes += data.length;
|
|
1760
|
+
// Start writing to temp file if exceeds threshold
|
|
1761
|
+
if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
|
|
1762
|
+
const id = randomBytes(8).toString("hex");
|
|
1763
|
+
tempFilePath = join(tmpdir(), `pi-bash-${id}.log`);
|
|
1764
|
+
tempFileStream = createWriteStream(tempFilePath);
|
|
1765
|
+
for (const chunk of chunks) {
|
|
1766
|
+
tempFileStream.write(chunk);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
if (tempFileStream) {
|
|
1770
|
+
tempFileStream.write(data);
|
|
1771
|
+
}
|
|
1772
|
+
// Keep rolling buffer
|
|
1773
|
+
chunks.push(data);
|
|
1774
|
+
chunksBytes += data.length;
|
|
1775
|
+
while (chunksBytes > maxChunksBytes && chunks.length > 1) {
|
|
1776
|
+
const removed = chunks.shift();
|
|
1777
|
+
chunksBytes -= removed.length;
|
|
1778
|
+
}
|
|
1779
|
+
// Stream to component (strip ANSI)
|
|
1780
|
+
const text = stripAnsi(data.toString()).replace(/\r/g, "");
|
|
1781
|
+
onChunk(text);
|
|
1782
|
+
};
|
|
1783
|
+
child.stdout?.on("data", handleData);
|
|
1784
|
+
child.stderr?.on("data", handleData);
|
|
1785
|
+
child.on("close", (code) => {
|
|
1786
|
+
if (tempFileStream) {
|
|
1787
|
+
tempFileStream.end();
|
|
1788
|
+
}
|
|
1789
|
+
this.bashProcess = null;
|
|
1790
|
+
// Combine buffered chunks for truncation
|
|
1791
|
+
const fullBuffer = Buffer.concat(chunks);
|
|
1792
|
+
const fullOutput = stripAnsi(fullBuffer.toString("utf-8")).replace(/\r/g, "");
|
|
1793
|
+
const truncationResult = truncateTail(fullOutput);
|
|
1794
|
+
// code === null means killed (cancelled)
|
|
1795
|
+
const cancelled = code === null;
|
|
1796
|
+
resolve({
|
|
1797
|
+
exitCode: code,
|
|
1798
|
+
cancelled,
|
|
1799
|
+
truncationResult: truncationResult.truncated ? truncationResult : undefined,
|
|
1800
|
+
fullOutputPath: tempFilePath,
|
|
1801
|
+
});
|
|
1802
|
+
});
|
|
1803
|
+
child.on("error", (err) => {
|
|
1804
|
+
if (tempFileStream) {
|
|
1805
|
+
tempFileStream.end();
|
|
1806
|
+
}
|
|
1807
|
+
this.bashProcess = null;
|
|
1808
|
+
reject(err);
|
|
1809
|
+
});
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1597
1812
|
compactionAbortController = null;
|
|
1598
1813
|
/**
|
|
1599
1814
|
* Shared logic to execute context compaction.
|