mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 03:01:56 +00:00
Add bash mode for executing shell commands
- Add ! prefix in TUI editor to execute shell commands directly
- Output streams in real-time and is added to LLM context
- Supports multiline commands, cancellation (Escape), truncation
- Preview mode shows last 20 lines, Ctrl+O expands full output
- Commands persist in session history as bashExecution messages
- Add bash command to RPC mode via {type:'bash',command:'...'}
- Add RPC tests for bash command execution and context inclusion
- Update docs: rpc.md, session.md, README.md, CHANGELOG.md
Closes #112
Co-authored-by: Markus Ylisiurunen <markus.ylisiurunen@gmail.com>
This commit is contained in:
parent
1608da8770
commit
bd0d0676d4
13 changed files with 917 additions and 126 deletions
|
|
@ -4,6 +4,7 @@ import { existsSync, readFileSync, writeFileSync } from "fs";
|
|||
import { homedir } from "os";
|
||||
import { basename } from "path";
|
||||
import { APP_NAME, VERSION } from "./config.js";
|
||||
import { type BashExecutionMessage, isBashExecutionMessage } from "./messages.js";
|
||||
import type { SessionManager } from "./session-manager.js";
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -56,6 +57,8 @@ const COLORS = {
|
|||
toolPendingBg: "rgb(40, 40, 50)",
|
||||
toolSuccessBg: "rgb(40, 50, 40)",
|
||||
toolErrorBg: "rgb(60, 40, 40)",
|
||||
userBashBg: "rgb(50, 48, 35)", // Faint yellow/brown for user-executed bash
|
||||
userBashErrorBg: "rgb(60, 45, 35)", // Slightly more orange for errors
|
||||
bodyBg: "rgb(24, 24, 30)",
|
||||
containerBg: "rgb(30, 30, 36)",
|
||||
text: "rgb(229, 229, 231)",
|
||||
|
|
@ -94,6 +97,34 @@ function formatTimestamp(timestamp: number | string | undefined): string {
|
|||
return date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
||||
}
|
||||
|
||||
function formatExpandableOutput(lines: string[], maxLines: number): string {
|
||||
const displayLines = lines.slice(0, maxLines);
|
||||
const remaining = lines.length - maxLines;
|
||||
|
||||
if (remaining > 0) {
|
||||
let out = '<div class="tool-output expandable" onclick="this.classList.toggle(\'expanded\')">';
|
||||
out += '<div class="output-preview">';
|
||||
for (const line of displayLines) {
|
||||
out += `<div>${escapeHtml(replaceTabs(line))}</div>`;
|
||||
}
|
||||
out += `<div class="expand-hint">... (${remaining} more lines) - click to expand</div>`;
|
||||
out += "</div>";
|
||||
out += '<div class="output-full">';
|
||||
for (const line of lines) {
|
||||
out += `<div>${escapeHtml(replaceTabs(line))}</div>`;
|
||||
}
|
||||
out += "</div></div>";
|
||||
return out;
|
||||
}
|
||||
|
||||
let out = '<div class="tool-output">';
|
||||
for (const line of displayLines) {
|
||||
out += `<div>${escapeHtml(replaceTabs(line))}</div>`;
|
||||
}
|
||||
out += "</div>";
|
||||
return out;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Parsing functions
|
||||
// ============================================================================
|
||||
|
|
@ -304,34 +335,6 @@ function formatToolExecution(
|
|||
return textBlocks.map((c) => (c as { type: "text"; text: string }).text).join("\n");
|
||||
};
|
||||
|
||||
const formatExpandableOutput = (lines: string[], maxLines: number): string => {
|
||||
const displayLines = lines.slice(0, maxLines);
|
||||
const remaining = lines.length - maxLines;
|
||||
|
||||
if (remaining > 0) {
|
||||
let out = '<div class="tool-output expandable" onclick="this.classList.toggle(\'expanded\')">';
|
||||
out += '<div class="output-preview">';
|
||||
for (const line of displayLines) {
|
||||
out += `<div>${escapeHtml(replaceTabs(line))}</div>`;
|
||||
}
|
||||
out += `<div class="expand-hint">... (${remaining} more lines) - click to expand</div>`;
|
||||
out += "</div>";
|
||||
out += '<div class="output-full">';
|
||||
for (const line of lines) {
|
||||
out += `<div>${escapeHtml(replaceTabs(line))}</div>`;
|
||||
}
|
||||
out += "</div></div>";
|
||||
return out;
|
||||
}
|
||||
|
||||
let out = '<div class="tool-output">';
|
||||
for (const line of displayLines) {
|
||||
out += `<div>${escapeHtml(replaceTabs(line))}</div>`;
|
||||
}
|
||||
out += "</div>";
|
||||
return out;
|
||||
};
|
||||
|
||||
switch (toolName) {
|
||||
case "bash": {
|
||||
const command = (args?.command as string) || "";
|
||||
|
|
@ -427,6 +430,35 @@ function formatMessage(message: Message, toolResultsMap: Map<string, ToolResultM
|
|||
const timestamp = (message as { timestamp?: number }).timestamp;
|
||||
const timestampHtml = timestamp ? `<div class="message-timestamp">${formatTimestamp(timestamp)}</div>` : "";
|
||||
|
||||
// Handle bash execution messages (user-executed via ! command)
|
||||
if (isBashExecutionMessage(message)) {
|
||||
const bashMsg = message as unknown as BashExecutionMessage;
|
||||
const isError = bashMsg.cancelled || (bashMsg.exitCode !== 0 && bashMsg.exitCode !== null);
|
||||
const bgColor = isError ? COLORS.userBashErrorBg : COLORS.userBashBg;
|
||||
|
||||
html += `<div class="tool-execution" style="background-color: ${bgColor}">`;
|
||||
html += timestampHtml;
|
||||
html += `<div class="tool-command">$ ${escapeHtml(bashMsg.command)}</div>`;
|
||||
|
||||
if (bashMsg.output) {
|
||||
const lines = bashMsg.output.split("\n");
|
||||
html += formatExpandableOutput(lines, 10);
|
||||
}
|
||||
|
||||
if (bashMsg.cancelled) {
|
||||
html += `<div class="bash-status" style="color: ${COLORS.yellow}">(cancelled)</div>`;
|
||||
} else if (bashMsg.exitCode !== 0 && bashMsg.exitCode !== null) {
|
||||
html += `<div class="bash-status" style="color: ${COLORS.red}">(exit ${bashMsg.exitCode})</div>`;
|
||||
}
|
||||
|
||||
if (bashMsg.truncated && bashMsg.fullOutputPath) {
|
||||
html += `<div class="bash-truncation" style="color: ${COLORS.yellow}">Output truncated. Full output: ${escapeHtml(bashMsg.fullOutputPath)}</div>`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
return html;
|
||||
}
|
||||
|
||||
if (message.role === "user") {
|
||||
const userMsg = message as UserMessage;
|
||||
let textContent = "";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue