agent-sh 0.1.0
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/README.md +659 -0
- package/dist/acp-client.d.ts +76 -0
- package/dist/acp-client.js +507 -0
- package/dist/context-manager.d.ts +45 -0
- package/dist/context-manager.js +405 -0
- package/dist/core.d.ts +41 -0
- package/dist/core.js +76 -0
- package/dist/event-bus.d.ts +140 -0
- package/dist/event-bus.js +79 -0
- package/dist/executor.d.ts +31 -0
- package/dist/executor.js +116 -0
- package/dist/extension-loader.d.ts +16 -0
- package/dist/extension-loader.js +164 -0
- package/dist/extensions/file-autocomplete.d.ts +2 -0
- package/dist/extensions/file-autocomplete.js +63 -0
- package/dist/extensions/shell-recall.d.ts +9 -0
- package/dist/extensions/shell-recall.js +8 -0
- package/dist/extensions/slash-commands.d.ts +2 -0
- package/dist/extensions/slash-commands.js +105 -0
- package/dist/extensions/tui-renderer.d.ts +2 -0
- package/dist/extensions/tui-renderer.js +354 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +159 -0
- package/dist/input-handler.d.ts +48 -0
- package/dist/input-handler.js +302 -0
- package/dist/output-parser.d.ts +55 -0
- package/dist/output-parser.js +166 -0
- package/dist/shell.d.ts +54 -0
- package/dist/shell.js +219 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.js +1 -0
- package/dist/utils/ansi.d.ts +12 -0
- package/dist/utils/ansi.js +23 -0
- package/dist/utils/box-frame.d.ts +21 -0
- package/dist/utils/box-frame.js +60 -0
- package/dist/utils/diff-renderer.d.ts +20 -0
- package/dist/utils/diff-renderer.js +506 -0
- package/dist/utils/diff.d.ts +24 -0
- package/dist/utils/diff.js +122 -0
- package/dist/utils/file-watcher.d.ts +31 -0
- package/dist/utils/file-watcher.js +101 -0
- package/dist/utils/markdown.d.ts +39 -0
- package/dist/utils/markdown.js +248 -0
- package/dist/utils/palette.d.ts +32 -0
- package/dist/utils/palette.js +36 -0
- package/dist/utils/tool-display.d.ts +33 -0
- package/dist/utils/tool-display.js +141 -0
- package/examples/extensions/interactive-prompts.ts +161 -0
- package/examples/extensions/solarized-theme.ts +27 -0
- package/package.json +72 -0
package/dist/shell.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as pty from "node-pty";
|
|
5
|
+
import { InputHandler } from "./input-handler.js";
|
|
6
|
+
import { OutputParser } from "./output-parser.js";
|
|
7
|
+
export class Shell {
|
|
8
|
+
ptyProcess;
|
|
9
|
+
bus;
|
|
10
|
+
inputHandler;
|
|
11
|
+
outputParser;
|
|
12
|
+
paused = false;
|
|
13
|
+
agentActive = false;
|
|
14
|
+
tmpDir;
|
|
15
|
+
constructor(opts) {
|
|
16
|
+
// Build environment — filter out undefined values (node-pty's native
|
|
17
|
+
// posix_spawnp fails if any env value is undefined)
|
|
18
|
+
const env = {};
|
|
19
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
20
|
+
if (v !== undefined)
|
|
21
|
+
env[k] = v;
|
|
22
|
+
}
|
|
23
|
+
env.AGENT_SH = "1";
|
|
24
|
+
// Spawn the user's shell with their full config (aliases, plugins, PATH,
|
|
25
|
+
// completions, etc.). The core injects three invisible OSC hooks:
|
|
26
|
+
// - OSC 7: cwd tracking (required by OutputParser)
|
|
27
|
+
// - OSC 9999: prompt start marker (command boundary detection)
|
|
28
|
+
// - OSC 9998: prompt end marker (bracketed prompt capture)
|
|
29
|
+
// Prompt theming is left entirely to the user's shell config.
|
|
30
|
+
const shellName = path.basename(opts.shell);
|
|
31
|
+
const isZsh = shellName.includes("zsh");
|
|
32
|
+
const isBash = shellName.includes("bash");
|
|
33
|
+
if (!isZsh && !isBash) {
|
|
34
|
+
console.warn(`Warning: agent-sh only supports zsh and bash. ` +
|
|
35
|
+
`"${opts.shell}" may not work correctly — falling back to /bin/bash.`);
|
|
36
|
+
}
|
|
37
|
+
const shellBin = (isZsh || isBash) ? opts.shell : "/bin/bash";
|
|
38
|
+
let shellArgs;
|
|
39
|
+
const osc7Cmd = 'printf "\\e]7;file://%s%s\\a" "$(hostname)" "$PWD"';
|
|
40
|
+
const promptMarker = 'printf "\\e]9999;PROMPT\\a"';
|
|
41
|
+
if (isZsh) {
|
|
42
|
+
// For zsh: use ZDOTDIR to source user's real config, then append
|
|
43
|
+
// our hooks via precmd_functions (additive — doesn't clobber p10k/omz).
|
|
44
|
+
this.tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-sh-"));
|
|
45
|
+
const userZdotdir = env.ZDOTDIR || env.HOME || os.homedir();
|
|
46
|
+
fs.writeFileSync(path.join(this.tmpDir, ".zshrc"), [
|
|
47
|
+
`ZDOTDIR="${userZdotdir}"`,
|
|
48
|
+
`[ -f "${userZdotdir}/.zshrc" ] && source "${userZdotdir}/.zshrc"`,
|
|
49
|
+
"",
|
|
50
|
+
"# agent-sh hooks (invisible OSC sequences for cwd + prompt detection)",
|
|
51
|
+
"__agent_sh_precmd() {",
|
|
52
|
+
` ${osc7Cmd}`,
|
|
53
|
+
` ${promptMarker}`,
|
|
54
|
+
"}",
|
|
55
|
+
"precmd_functions+=(__agent_sh_precmd)",
|
|
56
|
+
"",
|
|
57
|
+
"# End-of-prompt marker via zle-line-init (fires after prompt is rendered)",
|
|
58
|
+
"# Chain onto existing widget (p10k uses zle-line-init) rather than clobbering",
|
|
59
|
+
'if (( ${+widgets[zle-line-init]} )); then',
|
|
60
|
+
" zle -A zle-line-init __agent_sh_orig_line_init",
|
|
61
|
+
" __agent_sh_line_init() {",
|
|
62
|
+
" zle __agent_sh_orig_line_init",
|
|
63
|
+
' printf "\\e]9998;READY\\a"',
|
|
64
|
+
" }",
|
|
65
|
+
"else",
|
|
66
|
+
" __agent_sh_line_init() {",
|
|
67
|
+
' printf "\\e]9998;READY\\a"',
|
|
68
|
+
" }",
|
|
69
|
+
"fi",
|
|
70
|
+
"zle -N zle-line-init __agent_sh_line_init",
|
|
71
|
+
].join("\n") + "\n");
|
|
72
|
+
env.ZDOTDIR = this.tmpDir;
|
|
73
|
+
shellArgs = ["--no-globalrcs"];
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
// For bash: use --rcfile to source our wrapper, which sources the user's
|
|
77
|
+
// real bashrc then appends our hooks. No HOME override needed.
|
|
78
|
+
this.tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-sh-"));
|
|
79
|
+
const userHome = env.HOME || os.homedir();
|
|
80
|
+
fs.writeFileSync(path.join(this.tmpDir, ".bashrc"), [
|
|
81
|
+
`[ -f "${userHome}/.bashrc" ] && source "${userHome}/.bashrc"`,
|
|
82
|
+
"",
|
|
83
|
+
"# agent-sh hooks (invisible OSC sequences for cwd + prompt detection)",
|
|
84
|
+
`PROMPT_COMMAND="\${PROMPT_COMMAND:+\$PROMPT_COMMAND;}${osc7Cmd}; ${promptMarker}"`,
|
|
85
|
+
"",
|
|
86
|
+
"# End-of-prompt marker: append to PS1 (\\[...\\] marks it zero-width)",
|
|
87
|
+
'case "$PS1" in *9998*) ;; *) PS1="${PS1}\\[\\e]9998;READY\\a\\]";; esac',
|
|
88
|
+
].join("\n") + "\n");
|
|
89
|
+
shellArgs = ["--rcfile", path.join(this.tmpDir, ".bashrc")];
|
|
90
|
+
}
|
|
91
|
+
this.ptyProcess = pty.spawn(shellBin, shellArgs, {
|
|
92
|
+
name: "xterm-256color",
|
|
93
|
+
cols: opts.cols,
|
|
94
|
+
rows: opts.rows,
|
|
95
|
+
cwd: opts.cwd,
|
|
96
|
+
env,
|
|
97
|
+
});
|
|
98
|
+
this.bus = opts.bus;
|
|
99
|
+
this.outputParser = new OutputParser(opts.bus, opts.cwd);
|
|
100
|
+
// Ensure temp dir cleanup on abnormal exit (SIGKILL won't fire this,
|
|
101
|
+
// but it covers uncaught exceptions and normal process.exit paths)
|
|
102
|
+
if (this.tmpDir) {
|
|
103
|
+
const dir = this.tmpDir;
|
|
104
|
+
process.on("exit", () => {
|
|
105
|
+
try {
|
|
106
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
catch { }
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
this.inputHandler = new InputHandler({
|
|
112
|
+
ctx: this,
|
|
113
|
+
bus: opts.bus,
|
|
114
|
+
onShowAgentInfo: opts.onShowAgentInfo ?? (() => ({ info: "" })),
|
|
115
|
+
});
|
|
116
|
+
this.setupOutput();
|
|
117
|
+
this.setupInput();
|
|
118
|
+
this.setupAgentLifecycle();
|
|
119
|
+
}
|
|
120
|
+
// ── InputContext implementation (delegates to OutputParser) ──
|
|
121
|
+
isForegroundBusy() {
|
|
122
|
+
return this.outputParser.isForegroundBusy();
|
|
123
|
+
}
|
|
124
|
+
getCwd() {
|
|
125
|
+
return this.outputParser.getCwd();
|
|
126
|
+
}
|
|
127
|
+
isAgentActive() {
|
|
128
|
+
return this.agentActive;
|
|
129
|
+
}
|
|
130
|
+
writeToPty(data) {
|
|
131
|
+
this.ptyProcess.write(data);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Lightweight redraw: replay just the last line of the shell's prompt
|
|
135
|
+
* (e.g. p10k's "❯ "). This works because agent input mode only overwrites
|
|
136
|
+
* the final prompt line — the path bar above is still intact. The last
|
|
137
|
+
* line is linear text (colors + chars + clear-to-end), no cursor positioning.
|
|
138
|
+
*/
|
|
139
|
+
redrawPrompt() {
|
|
140
|
+
const result = this.bus.emitPipe("shell:redraw-prompt", {
|
|
141
|
+
cwd: this.outputParser.getCwd(),
|
|
142
|
+
handled: false,
|
|
143
|
+
});
|
|
144
|
+
if (!result.handled) {
|
|
145
|
+
const lastLine = this.outputParser.getLastPromptLine();
|
|
146
|
+
if (lastLine) {
|
|
147
|
+
process.stdout.write("\r" + lastLine);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
// Fallback: send \n for a fresh prompt cycle
|
|
151
|
+
this.ptyProcess.write("\n");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Heavy redraw: send \n to PTY to trigger a full precmd → prompt cycle.
|
|
157
|
+
* Use this after agent responses where stdout has moved far from where
|
|
158
|
+
* zle expects the cursor. The blank line is acceptable as a separator.
|
|
159
|
+
*/
|
|
160
|
+
freshPrompt() {
|
|
161
|
+
this.ptyProcess.write("\n");
|
|
162
|
+
}
|
|
163
|
+
onCommandEntered(command, cwd) {
|
|
164
|
+
this.outputParser.onCommandEntered(command, cwd);
|
|
165
|
+
}
|
|
166
|
+
// ── PTY I/O wiring ─────────────────────────────────────────
|
|
167
|
+
setupOutput() {
|
|
168
|
+
this.ptyProcess.onData((data) => {
|
|
169
|
+
this.outputParser.processData(data);
|
|
170
|
+
if (!this.paused) {
|
|
171
|
+
process.stdout.write(data);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
setupInput() {
|
|
176
|
+
process.stdin.on("data", (data) => {
|
|
177
|
+
const str = data.toString("utf-8");
|
|
178
|
+
this.inputHandler.handleInput(str);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* React to agent lifecycle events — Shell manages its own state
|
|
183
|
+
* rather than being driven by AcpClient. This means AcpClient has
|
|
184
|
+
* zero frontend knowledge; any frontend can subscribe to the same events.
|
|
185
|
+
*/
|
|
186
|
+
setupAgentLifecycle() {
|
|
187
|
+
this.bus.on("agent:processing-start", () => {
|
|
188
|
+
this.agentActive = true;
|
|
189
|
+
this.paused = true;
|
|
190
|
+
});
|
|
191
|
+
this.bus.on("agent:processing-done", () => {
|
|
192
|
+
this.paused = false;
|
|
193
|
+
this.agentActive = false;
|
|
194
|
+
this.freshPrompt();
|
|
195
|
+
});
|
|
196
|
+
// Permission prompts need stdout unpaused so the interactive UI renders,
|
|
197
|
+
// then re-paused after the decision.
|
|
198
|
+
this.bus.on("permission:request", () => {
|
|
199
|
+
this.paused = false;
|
|
200
|
+
});
|
|
201
|
+
this.bus.onPipeAsync("permission:request", async (payload) => {
|
|
202
|
+
this.paused = true;
|
|
203
|
+
return payload;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
// ── Public API (used by index.ts) ──
|
|
207
|
+
resize(cols, rows) {
|
|
208
|
+
this.ptyProcess.resize(cols, rows);
|
|
209
|
+
}
|
|
210
|
+
onExit(callback) {
|
|
211
|
+
this.ptyProcess.onExit(callback);
|
|
212
|
+
}
|
|
213
|
+
kill() {
|
|
214
|
+
this.ptyProcess.kill();
|
|
215
|
+
if (this.tmpDir) {
|
|
216
|
+
fs.rmSync(this.tmpDir, { recursive: true, force: true });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { EventBus } from "./event-bus.js";
|
|
2
|
+
import type { ContextManager } from "./context-manager.js";
|
|
3
|
+
import type { AcpClient } from "./acp-client.js";
|
|
4
|
+
import type { ColorPalette } from "./utils/palette.js";
|
|
5
|
+
export interface AgentShellConfig {
|
|
6
|
+
agentCommand: string;
|
|
7
|
+
agentArgs: string[];
|
|
8
|
+
shell?: string;
|
|
9
|
+
model?: string;
|
|
10
|
+
extensions?: string[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Context passed to user/third-party extensions.
|
|
14
|
+
* Extensions interact with the system through the event bus — no direct
|
|
15
|
+
* frontend (Shell/TUI) dependencies. This enables headless, web, or
|
|
16
|
+
* alternative frontends without changing extensions.
|
|
17
|
+
*/
|
|
18
|
+
export interface ExtensionContext {
|
|
19
|
+
bus: EventBus;
|
|
20
|
+
contextManager: ContextManager;
|
|
21
|
+
getAcpClient: () => AcpClient;
|
|
22
|
+
quit: () => void;
|
|
23
|
+
/** Override color palette slots for theming. */
|
|
24
|
+
setPalette: (overrides: Partial<ColorPalette>) => void;
|
|
25
|
+
}
|
|
26
|
+
export interface TerminalSession {
|
|
27
|
+
id: string;
|
|
28
|
+
command: string;
|
|
29
|
+
output: string;
|
|
30
|
+
exitCode: number | null;
|
|
31
|
+
done: boolean;
|
|
32
|
+
resolve?: (value: void) => void;
|
|
33
|
+
}
|
|
34
|
+
export interface ToolCallRecord {
|
|
35
|
+
tool: string;
|
|
36
|
+
args: Record<string, unknown>;
|
|
37
|
+
output: string;
|
|
38
|
+
exitCode: number | null;
|
|
39
|
+
}
|
|
40
|
+
export type Exchange = {
|
|
41
|
+
type: "shell_command";
|
|
42
|
+
id: number;
|
|
43
|
+
timestamp: number;
|
|
44
|
+
cwd: string;
|
|
45
|
+
command: string;
|
|
46
|
+
output: string;
|
|
47
|
+
exitCode: number | null;
|
|
48
|
+
outputLines: number;
|
|
49
|
+
outputBytes: number;
|
|
50
|
+
} | {
|
|
51
|
+
type: "agent_query";
|
|
52
|
+
id: number;
|
|
53
|
+
timestamp: number;
|
|
54
|
+
query: string;
|
|
55
|
+
} | {
|
|
56
|
+
type: "agent_response";
|
|
57
|
+
id: number;
|
|
58
|
+
timestamp: number;
|
|
59
|
+
response: string;
|
|
60
|
+
toolCalls: ToolCallRecord[];
|
|
61
|
+
} | {
|
|
62
|
+
type: "tool_execution";
|
|
63
|
+
id: number;
|
|
64
|
+
timestamp: number;
|
|
65
|
+
tool: string;
|
|
66
|
+
args: Record<string, unknown>;
|
|
67
|
+
output: string;
|
|
68
|
+
exitCode: number | null;
|
|
69
|
+
outputLines: number;
|
|
70
|
+
outputBytes: number;
|
|
71
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const CYAN = "\u001B[36m";
|
|
2
|
+
export declare const DIM = "\u001B[2m";
|
|
3
|
+
export declare const YELLOW = "\u001B[33m";
|
|
4
|
+
export declare const GREEN = "\u001B[32m";
|
|
5
|
+
export declare const RED = "\u001B[31m";
|
|
6
|
+
export declare const GRAY = "\u001B[90m";
|
|
7
|
+
export declare const BOLD = "\u001B[1m";
|
|
8
|
+
export declare const RESET = "\u001B[0m";
|
|
9
|
+
/** Measure visible string length, excluding SGR (color/style) sequences. */
|
|
10
|
+
export declare function visibleLen(str: string): number;
|
|
11
|
+
/** Strip all ANSI escape sequences (SGR, OSC, CSI, private mode) and carriage returns. */
|
|
12
|
+
export declare function stripAnsi(str: string): string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// ── ANSI escape code constants ────────────────────────────────
|
|
2
|
+
export const CYAN = "\x1b[36m";
|
|
3
|
+
export const DIM = "\x1b[2m";
|
|
4
|
+
export const YELLOW = "\x1b[33m";
|
|
5
|
+
export const GREEN = "\x1b[32m";
|
|
6
|
+
export const RED = "\x1b[31m";
|
|
7
|
+
export const GRAY = "\x1b[90m";
|
|
8
|
+
export const BOLD = "\x1b[1m";
|
|
9
|
+
export const RESET = "\x1b[0m";
|
|
10
|
+
// ── ANSI utility functions ───────────────────────────────────
|
|
11
|
+
/** Measure visible string length, excluding SGR (color/style) sequences. */
|
|
12
|
+
export function visibleLen(str) {
|
|
13
|
+
return str.replace(/\x1b\[[^m]*m/g, "").length;
|
|
14
|
+
}
|
|
15
|
+
/** Strip all ANSI escape sequences (SGR, OSC, CSI, private mode) and carriage returns. */
|
|
16
|
+
export function stripAnsi(str) {
|
|
17
|
+
return str
|
|
18
|
+
.replace(/\x1b\][^\x07]*\x07/g, "") // OSC sequences
|
|
19
|
+
.replace(/\x1b\[[^m]*m/g, "") // SGR (color) sequences
|
|
20
|
+
.replace(/\x1b\[\?[^a-zA-Z]*[a-zA-Z]/g, "") // private mode sequences
|
|
21
|
+
.replace(/\x1b\[[^a-zA-Z]*[a-zA-Z]/g, "") // CSI sequences
|
|
22
|
+
.replace(/\r/g, ""); // carriage returns
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type BorderStyle = "rounded" | "square" | "double" | "heavy";
|
|
2
|
+
export interface BoxFrameOptions {
|
|
3
|
+
/** Total width including borders. */
|
|
4
|
+
width: number;
|
|
5
|
+
/** Border style. Default "rounded". */
|
|
6
|
+
style?: BorderStyle;
|
|
7
|
+
/** Border color (ANSI escape). Default DIM. */
|
|
8
|
+
borderColor?: string;
|
|
9
|
+
/** Title text shown in the top border. */
|
|
10
|
+
title?: string;
|
|
11
|
+
/** Footer lines shown below a divider, inside the box. */
|
|
12
|
+
footer?: string[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Render content lines inside a bordered frame.
|
|
16
|
+
*
|
|
17
|
+
* @param content - Array of pre-rendered content lines (no border)
|
|
18
|
+
* @param opts - Frame options
|
|
19
|
+
* @returns Array of terminal-ready lines with borders
|
|
20
|
+
*/
|
|
21
|
+
export declare function renderBoxFrame(content: string[], opts: BoxFrameOptions): string[];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Box frame component for bordered TUI panels.
|
|
3
|
+
*
|
|
4
|
+
* Follows the render(width) -> string[] protocol — pure function,
|
|
5
|
+
* never writes to stdout. Supports multiple border styles and
|
|
6
|
+
* optional title/footer sections with dividers.
|
|
7
|
+
*/
|
|
8
|
+
import { visibleLen } from "./ansi.js";
|
|
9
|
+
import { palette as p } from "./palette.js";
|
|
10
|
+
const BORDERS = {
|
|
11
|
+
rounded: { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│", ml: "├", mr: "┤" },
|
|
12
|
+
square: { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", ml: "├", mr: "┤" },
|
|
13
|
+
double: { tl: "╔", tr: "╗", bl: "╚", br: "╝", h: "═", v: "║", ml: "╠", mr: "╣" },
|
|
14
|
+
heavy: { tl: "┏", tr: "┓", bl: "┗", br: "┛", h: "━", v: "┃", ml: "┣", mr: "┫" },
|
|
15
|
+
};
|
|
16
|
+
// ── Public API ───────────────────────────────────────────────────
|
|
17
|
+
/**
|
|
18
|
+
* Render content lines inside a bordered frame.
|
|
19
|
+
*
|
|
20
|
+
* @param content - Array of pre-rendered content lines (no border)
|
|
21
|
+
* @param opts - Frame options
|
|
22
|
+
* @returns Array of terminal-ready lines with borders
|
|
23
|
+
*/
|
|
24
|
+
export function renderBoxFrame(content, opts) {
|
|
25
|
+
const { width, borderColor = p.dim } = opts;
|
|
26
|
+
const style = opts.style ?? "rounded";
|
|
27
|
+
const b = BORDERS[style];
|
|
28
|
+
const bc = borderColor;
|
|
29
|
+
// Content area width = total - 2 borders - 2 padding spaces
|
|
30
|
+
const innerW = Math.max(1, width - 4);
|
|
31
|
+
const output = [];
|
|
32
|
+
// Top border (with optional title)
|
|
33
|
+
if (opts.title) {
|
|
34
|
+
const titleVis = visibleLen(opts.title);
|
|
35
|
+
const afterDashes = Math.max(1, width - titleVis - 4);
|
|
36
|
+
output.push(`${bc}${b.tl}${p.reset} ${opts.title} ${bc}${b.h.repeat(afterDashes)}${b.tr}${p.reset}`);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
output.push(`${bc}${b.tl}${b.h.repeat(width - 2)}${b.tr}${p.reset}`);
|
|
40
|
+
}
|
|
41
|
+
// Content lines
|
|
42
|
+
for (const line of content) {
|
|
43
|
+
output.push(boxLine(line, innerW, b.v, bc));
|
|
44
|
+
}
|
|
45
|
+
// Footer with divider
|
|
46
|
+
if (opts.footer && opts.footer.length > 0) {
|
|
47
|
+
output.push(`${bc}${b.ml}${b.h.repeat(width - 2)}${b.mr}${p.reset}`);
|
|
48
|
+
for (const line of opts.footer) {
|
|
49
|
+
output.push(boxLine(line, innerW, b.v, bc));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Bottom border
|
|
53
|
+
output.push(`${bc}${b.bl}${b.h.repeat(width - 2)}${b.br}${p.reset}`);
|
|
54
|
+
return output;
|
|
55
|
+
}
|
|
56
|
+
// ── Helpers ──────────────────────────────────────────────────────
|
|
57
|
+
function boxLine(text, innerW, v, bc) {
|
|
58
|
+
const pad = Math.max(0, innerW - visibleLen(text));
|
|
59
|
+
return `${bc}${v}${p.reset} ${text}${" ".repeat(pad)} ${bc}${v}${p.reset}`;
|
|
60
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DiffResult } from "./diff.js";
|
|
2
|
+
export type DiffDisplayMode = "split" | "unified" | "summary";
|
|
3
|
+
export interface DiffRenderOptions {
|
|
4
|
+
/** Available terminal width (columns). */
|
|
5
|
+
width: number;
|
|
6
|
+
/** Force a specific display mode instead of auto-detecting from width. */
|
|
7
|
+
mode?: DiffDisplayMode;
|
|
8
|
+
/** Maximum number of output lines before truncation. Default 50. */
|
|
9
|
+
maxLines?: number;
|
|
10
|
+
/** File path to show in the header (also used to detect language for syntax highlighting). */
|
|
11
|
+
filePath?: string;
|
|
12
|
+
/** Use true-color (24-bit) backgrounds. Default true. */
|
|
13
|
+
trueColor?: boolean;
|
|
14
|
+
/** Enable syntax highlighting on diff lines. Default true. */
|
|
15
|
+
syntaxHighlight?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** Select display mode based on available terminal width. */
|
|
18
|
+
export declare function selectMode(width: number): DiffDisplayMode;
|
|
19
|
+
/** Render a diff result as an array of ANSI-formatted terminal lines. */
|
|
20
|
+
export declare function renderDiff(diff: DiffResult, opts: DiffRenderOptions): string[];
|