agent-sh 0.15.0 → 0.15.2
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/dist/agent/agent-loop.js +11 -8
- package/dist/agent/events.d.ts +4 -0
- package/docs/README.md +14 -0
- package/docs/agent.md +398 -0
- package/docs/architecture.md +196 -0
- package/docs/context-management.md +200 -0
- package/docs/extensions.md +951 -0
- package/docs/library.md +84 -0
- package/docs/troubleshooting.md +65 -0
- package/docs/tui-composition.md +294 -0
- package/docs/usage.md +306 -0
- package/examples/extensions/ash-scheme/package.json +1 -1
- package/examples/extensions/ashi/EXTENDING.md +2 -2
- package/examples/extensions/ashi/README.md +2 -2
- package/examples/extensions/ashi/docs/ui-surface-protocol.md +1 -1
- package/examples/extensions/ashi/package.json +5 -3
- package/examples/extensions/ashi/src/chat/tool-group.ts +3 -2
- package/examples/extensions/ashi/src/cli.ts +9 -8
- package/examples/extensions/ashi/src/dialogs.ts +16 -1
- package/examples/extensions/ashi/src/events.ts +1 -0
- package/examples/extensions/ashi/src/frontend.ts +26 -6
- package/examples/extensions/ashi/src/renderer.ts +24 -4
- package/examples/extensions/ashi/src/renderers/pi-tui/schema-mount.ts +4 -3
- package/examples/extensions/ashi/src/renderers/pi-tui/tool-group.ts +5 -8
- package/examples/extensions/ashi/src/ui.ts +11 -0
- package/examples/extensions/ashi-ink/package.json +2 -2
- package/examples/extensions/claude-code-bridge/package.json +1 -1
- package/examples/extensions/opencode-bridge/package.json +1 -1
- package/package.json +3 -1
- package/src/agent/agent-loop.ts +1566 -0
- package/src/agent/entry-format.ts +19 -0
- package/src/agent/events.ts +153 -0
- package/src/agent/extensions/rolling-history/constants.ts +1 -0
- package/src/agent/extensions/rolling-history/index.ts +202 -0
- package/src/agent/extensions/rolling-history/recall.ts +131 -0
- package/src/agent/extensions/rolling-history/strategy.ts +404 -0
- package/src/agent/host-types.ts +192 -0
- package/src/agent/index.ts +591 -0
- package/src/agent/live-view.ts +279 -0
- package/src/agent/llm-client.ts +111 -0
- package/src/agent/llm-facade.ts +43 -0
- package/src/agent/normalize-args.ts +61 -0
- package/src/agent/nuclear-form.ts +382 -0
- package/src/agent/providers/deepseek.ts +39 -0
- package/src/agent/providers/ollama.ts +92 -0
- package/src/agent/providers/openai-compatible.ts +36 -0
- package/src/agent/providers/openai.ts +52 -0
- package/src/agent/providers/opencode.ts +142 -0
- package/src/agent/providers/openrouter.ts +105 -0
- package/src/agent/providers/zai-coding-plan.ts +33 -0
- package/src/agent/session-store.ts +336 -0
- package/src/agent/skills.ts +228 -0
- package/src/agent/store.ts +310 -0
- package/src/agent/subagent.ts +305 -0
- package/src/agent/system-prompt.ts +151 -0
- package/src/agent/token-budget.ts +12 -0
- package/src/agent/tool-protocol.ts +722 -0
- package/src/agent/tool-registry.ts +66 -0
- package/src/agent/tools/bash.ts +95 -0
- package/src/agent/tools/edit-file.ts +154 -0
- package/src/agent/tools/expand-home.ts +7 -0
- package/src/agent/tools/glob.ts +108 -0
- package/src/agent/tools/grep.ts +228 -0
- package/src/agent/tools/list-skills.ts +37 -0
- package/src/agent/tools/ls.ts +81 -0
- package/src/agent/tools/pwsh.ts +140 -0
- package/src/agent/tools/read-file.ts +164 -0
- package/src/agent/tools/write-file.ts +72 -0
- package/src/agent/types.ts +149 -0
- package/src/cli/args.ts +91 -0
- package/src/cli/auth/cli.ts +244 -0
- package/src/cli/auth/discover.ts +52 -0
- package/src/cli/auth/keys.ts +143 -0
- package/src/cli/index.ts +295 -0
- package/src/cli/init.ts +74 -0
- package/src/cli/install.ts +439 -0
- package/src/cli/shell-env.ts +68 -0
- package/src/cli/subcommands.ts +24 -0
- package/src/core/event-bus.ts +252 -0
- package/src/core/extension-loader.ts +347 -0
- package/src/core/index.ts +152 -0
- package/src/core/settings.ts +398 -0
- package/src/core/types.ts +61 -0
- package/src/extensions/file-autocomplete.ts +71 -0
- package/src/extensions/index.ts +38 -0
- package/src/extensions/slash-commands/events.ts +14 -0
- package/src/extensions/slash-commands/index.ts +269 -0
- package/src/shell/events.ts +73 -0
- package/src/shell/host-types.ts +150 -0
- package/src/shell/index.ts +159 -0
- package/src/shell/input-handler.ts +505 -0
- package/src/shell/output-parser.ts +156 -0
- package/src/shell/shell-context.ts +193 -0
- package/src/shell/shell.ts +414 -0
- package/src/shell/strategies/bash.ts +83 -0
- package/src/shell/strategies/fish.ts +77 -0
- package/src/shell/strategies/index.ts +24 -0
- package/src/shell/strategies/types.ts +64 -0
- package/src/shell/strategies/zsh.ts +92 -0
- package/src/shell/terminal.ts +124 -0
- package/src/shell/tui-input-view.ts +222 -0
- package/src/shell/tui-renderer.ts +1126 -0
- package/src/utils/ansi.ts +140 -0
- package/src/utils/box-frame.ts +138 -0
- package/src/utils/compositor.ts +157 -0
- package/src/utils/diff-renderer.ts +829 -0
- package/src/utils/diff.ts +244 -0
- package/src/utils/executor.ts +305 -0
- package/src/utils/file-watcher.ts +110 -0
- package/src/utils/floating-panel.ts +1160 -0
- package/src/utils/handler-registry.ts +110 -0
- package/src/utils/line-editor.ts +636 -0
- package/src/utils/markdown.ts +437 -0
- package/src/utils/message-utils.ts +113 -0
- package/src/utils/package-version.ts +12 -0
- package/src/utils/palette.ts +64 -0
- package/src/utils/ref-counter.ts +9 -0
- package/src/utils/ripgrep-path.ts +17 -0
- package/src/utils/shell-output-spill.ts +76 -0
- package/src/utils/stream-transform.ts +292 -0
- package/src/utils/terminal-buffer.ts +213 -0
- package/src/utils/tool-display.ts +315 -0
- package/src/utils/tool-interactive.ts +71 -0
- package/src/utils/tty.ts +14 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import type { ShellStrategy, PrepareSpawnOpts, ShellSpawnConfig } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const OSC7_CMD = 'printf "\\e]7;file://%s%s\\a" "$(hostname)" "$PWD"';
|
|
6
|
+
const TITLE_CMD = 'printf "\\e]0;⚡ agent-sh: %s\\a" "${PWD/#$HOME/~}"';
|
|
7
|
+
|
|
8
|
+
export const zshStrategy: ShellStrategy = {
|
|
9
|
+
name: "zsh",
|
|
10
|
+
|
|
11
|
+
matches(shellPath: string): boolean {
|
|
12
|
+
return path.basename(shellPath).includes("zsh");
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
prepareSpawn(opts: PrepareSpawnOpts): ShellSpawnConfig {
|
|
16
|
+
const { tmpDirRoot, instanceTag, showIndicator, env, userHome } = opts;
|
|
17
|
+
|
|
18
|
+
// Use ZDOTDIR to source user's real config, then append our hooks via
|
|
19
|
+
// precmd_functions (additive — doesn't clobber p10k/omz).
|
|
20
|
+
const tmpDir = fs.mkdtempSync(path.join(tmpDirRoot, "agent-sh-"));
|
|
21
|
+
const userZdotdir = env.ZDOTDIR || env.HOME || userHome;
|
|
22
|
+
const promptMarker = `printf "\\e]9999;${instanceTag};PROMPT\\a"`;
|
|
23
|
+
|
|
24
|
+
const lines = [
|
|
25
|
+
`ZDOTDIR="${userZdotdir}"`,
|
|
26
|
+
`[ -f "${userZdotdir}/.zshrc" ] && source "${userZdotdir}/.zshrc"`,
|
|
27
|
+
"",
|
|
28
|
+
"# agent-sh hooks (invisible OSC sequences for cwd + prompt detection)",
|
|
29
|
+
"__agent_sh_precmd() {",
|
|
30
|
+
` ${OSC7_CMD}`,
|
|
31
|
+
` ${promptMarker}`,
|
|
32
|
+
...(showIndicator ? [` ${TITLE_CMD}`] : []),
|
|
33
|
+
"}",
|
|
34
|
+
"precmd_functions+=(__agent_sh_precmd)",
|
|
35
|
+
"",
|
|
36
|
+
"# Preexec hook: emit actual command text so agent-sh can track",
|
|
37
|
+
"# history-recalled and tab-completed commands accurately",
|
|
38
|
+
"__agent_sh_preexec() {",
|
|
39
|
+
` printf "\\e]9997;${instanceTag};%s\\a" "$1"`,
|
|
40
|
+
"}",
|
|
41
|
+
"preexec_functions+=(__agent_sh_preexec)",
|
|
42
|
+
"",
|
|
43
|
+
"# End-of-prompt marker via zle-line-init (fires after prompt is rendered)",
|
|
44
|
+
"# Chain onto existing widget (p10k uses zle-line-init) rather than clobbering",
|
|
45
|
+
'if (( ${+widgets[zle-line-init]} )); then',
|
|
46
|
+
" zle -A zle-line-init __agent_sh_orig_line_init",
|
|
47
|
+
" __agent_sh_line_init() {",
|
|
48
|
+
" zle __agent_sh_orig_line_init",
|
|
49
|
+
` printf "\\e]9998;${instanceTag};READY\\a"`,
|
|
50
|
+
" }",
|
|
51
|
+
"else",
|
|
52
|
+
" __agent_sh_line_init() {",
|
|
53
|
+
` printf "\\e]9998;${instanceTag};READY\\a"`,
|
|
54
|
+
" }",
|
|
55
|
+
"fi",
|
|
56
|
+
"zle -N zle-line-init __agent_sh_line_init",
|
|
57
|
+
"",
|
|
58
|
+
"# Hidden widget to trigger prompt redraw from Node.js side",
|
|
59
|
+
"# Bound to an unused escape sequence that no real key produces",
|
|
60
|
+
"__agent_sh_redraw() {",
|
|
61
|
+
" zle reset-prompt",
|
|
62
|
+
"}",
|
|
63
|
+
"zle -N __agent_sh_redraw",
|
|
64
|
+
"bindkey '\\e[9999~' __agent_sh_redraw",
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
fs.writeFileSync(path.join(tmpDir, ".zshrc"), lines.join("\n") + "\n");
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
args: ["--no-globalrcs"],
|
|
71
|
+
envOverrides: { ZDOTDIR: tmpDir },
|
|
72
|
+
tmpDir,
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
envCaptureCommand(): string {
|
|
77
|
+
return "source ~/.zshrc 2>/dev/null; env -0";
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
redrawEscape(): string {
|
|
81
|
+
return "\x1b[9999~";
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
cleanOutput(raw: string): string {
|
|
85
|
+
return raw.replace(PROMPT_SP_RE, "");
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** PROMPT_SP marker (inverse-video PROMPT_EOL_MARK, default `%`) zsh prints
|
|
90
|
+
* before a prompt when prior output didn't end at column 0. Matching the
|
|
91
|
+
* inverse-video wrapper preserves legitimate trailing `%`. */
|
|
92
|
+
const PROMPT_SP_RE = /\x1b\[7m.\x1b\[(?:0|27)m/g;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal — the user-facing I/O endpoint that a Shell talks to.
|
|
3
|
+
*
|
|
4
|
+
* Shell wraps a *pseudo*-terminal (the PTY the child shell sees). This
|
|
5
|
+
* interface is the *real* terminal (or its substitute) on the other end:
|
|
6
|
+
* bytes in, bytes out, dimensions, resize notifications. The default
|
|
7
|
+
* factory wires it to process.stdin/stdout for the CLI; headless hosts
|
|
8
|
+
* (multi-session web hubs, tests) supply their own.
|
|
9
|
+
*/
|
|
10
|
+
import type { RenderSurface } from "../utils/compositor.js";
|
|
11
|
+
|
|
12
|
+
export interface Terminal {
|
|
13
|
+
write(data: string): void;
|
|
14
|
+
onInput(cb: (data: string) => void): () => void;
|
|
15
|
+
onResize(cb: (cols: number, rows: number) => void): () => void;
|
|
16
|
+
cols(): number;
|
|
17
|
+
rows(): number;
|
|
18
|
+
/**
|
|
19
|
+
* Called around PTY spawn to avoid TTY contention: the child PTY becomes
|
|
20
|
+
* the controlling tty for the spawned shell. No-op when the terminal
|
|
21
|
+
* isn't a real tty.
|
|
22
|
+
*/
|
|
23
|
+
suspendInput?(): { resume(): void };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Default Terminal: wraps process.stdin/stdout. */
|
|
27
|
+
export function processTerminal(): Terminal {
|
|
28
|
+
return {
|
|
29
|
+
write(data) {
|
|
30
|
+
if (process.stdout.writable) {
|
|
31
|
+
try { process.stdout.write(data); } catch { /* ignore */ }
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
onInput(cb) {
|
|
35
|
+
const handler = (b: Buffer) => cb(b.toString("utf-8"));
|
|
36
|
+
process.stdin.on("data", handler);
|
|
37
|
+
return () => { process.stdin.off("data", handler); };
|
|
38
|
+
},
|
|
39
|
+
onResize(cb) {
|
|
40
|
+
const handler = () => cb(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
41
|
+
process.stdout.on("resize", handler);
|
|
42
|
+
return () => { process.stdout.off("resize", handler); };
|
|
43
|
+
},
|
|
44
|
+
cols() { return process.stdout.columns || 80; },
|
|
45
|
+
rows() { return process.stdout.rows || 24; },
|
|
46
|
+
suspendInput() {
|
|
47
|
+
const wasRaw = process.stdin.isTTY && (process.stdin as { isRaw?: boolean }).isRaw;
|
|
48
|
+
if (process.stdin.isTTY) {
|
|
49
|
+
try {
|
|
50
|
+
process.stdin.setRawMode(false);
|
|
51
|
+
process.stdin.pause();
|
|
52
|
+
} catch { /* ignore */ }
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
resume() {
|
|
56
|
+
if (process.stdin.isTTY) {
|
|
57
|
+
try {
|
|
58
|
+
process.stdin.resume();
|
|
59
|
+
if (wasRaw) process.stdin.setRawMode(true);
|
|
60
|
+
} catch { /* ignore */ }
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* No-op terminal for non-rendering hosts (tests, agent-only embeds).
|
|
70
|
+
* Writes are discarded; input/resize never fire.
|
|
71
|
+
*/
|
|
72
|
+
export function headlessTerminal(cols = 100, rows = 30): Terminal {
|
|
73
|
+
return {
|
|
74
|
+
write() {},
|
|
75
|
+
onInput: () => () => {},
|
|
76
|
+
onResize: () => () => {},
|
|
77
|
+
cols: () => cols,
|
|
78
|
+
rows: () => rows,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Pipe-based terminal for embedders that own their own renderer (web hubs
|
|
84
|
+
* via xterm.js, electron windows, recording harnesses). Bytes from the
|
|
85
|
+
* Shell flow through `onWrite`; the host drives `pushInput`/`pushResize`
|
|
86
|
+
* to forward keystrokes and viewport changes back.
|
|
87
|
+
*/
|
|
88
|
+
export class BridgedTerminal implements Terminal {
|
|
89
|
+
private inputCbs = new Set<(d: string) => void>();
|
|
90
|
+
private resizeCbs = new Set<(c: number, r: number) => void>();
|
|
91
|
+
private _cols: number;
|
|
92
|
+
private _rows: number;
|
|
93
|
+
constructor(private readonly onWrite: (data: string) => void, cols = 100, rows = 30) {
|
|
94
|
+
this._cols = cols;
|
|
95
|
+
this._rows = rows;
|
|
96
|
+
}
|
|
97
|
+
write(data: string): void { this.onWrite(data); }
|
|
98
|
+
onInput(cb: (d: string) => void): () => void { this.inputCbs.add(cb); return () => { this.inputCbs.delete(cb); }; }
|
|
99
|
+
onResize(cb: (c: number, r: number) => void): () => void { this.resizeCbs.add(cb); return () => { this.resizeCbs.delete(cb); }; }
|
|
100
|
+
cols(): number { return this._cols; }
|
|
101
|
+
rows(): number { return this._rows; }
|
|
102
|
+
pushInput(data: string): void { for (const cb of this.inputCbs) cb(data); }
|
|
103
|
+
pushResize(cols: number, rows: number): void {
|
|
104
|
+
this._cols = cols;
|
|
105
|
+
this._rows = rows;
|
|
106
|
+
for (const cb of this.resizeCbs) cb(cols, rows);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Adapt a Terminal to a RenderSurface (the compositor's sink type). Adds
|
|
112
|
+
* the OPOST-cleared `\n` → `\r\n` translation that StdoutSurface applies,
|
|
113
|
+
* since the PTY has OPOST disabled.
|
|
114
|
+
*/
|
|
115
|
+
export function surfaceFromTerminal(terminal: Terminal): RenderSurface {
|
|
116
|
+
const write = (text: string) => terminal.write(text.replace(/(?<!\r)\n/g, "\r\n"));
|
|
117
|
+
return {
|
|
118
|
+
write,
|
|
119
|
+
writeLine: (line) => write(line + "\n"),
|
|
120
|
+
get columns() { return terminal.cols(); },
|
|
121
|
+
get rows() { return terminal.rows(); },
|
|
122
|
+
onResize: (cb) => terminal.onResize(cb),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal renderer for the input-mode prompt and autocomplete dropdown.
|
|
3
|
+
* Owns screen state (cursor row/col, autocomplete line count) and the
|
|
4
|
+
* ANSI redraw. The controller drives it via a small VM shape.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { visibleLen, truncateToWidth } from "../utils/ansi.js";
|
|
8
|
+
import { palette as p } from "../utils/palette.js";
|
|
9
|
+
import type { RenderSurface } from "../utils/compositor.js";
|
|
10
|
+
import { StdoutSurface } from "../utils/compositor.js";
|
|
11
|
+
|
|
12
|
+
export interface PromptVM {
|
|
13
|
+
showBuffer: boolean;
|
|
14
|
+
displayText: string;
|
|
15
|
+
displayCursor: number;
|
|
16
|
+
indicator: string;
|
|
17
|
+
promptIcon: string;
|
|
18
|
+
agentInfo: { info: string };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AutocompleteVM {
|
|
22
|
+
items: { name: string; description: string }[];
|
|
23
|
+
selected: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class TuiInputView {
|
|
27
|
+
private cursorRowsBelow = 0;
|
|
28
|
+
private cursorTermCol = 1;
|
|
29
|
+
private autocompleteLines = 0;
|
|
30
|
+
private readonly surface: RenderSurface;
|
|
31
|
+
private frameBuf: string | null = null;
|
|
32
|
+
|
|
33
|
+
constructor(surface?: RenderSurface) {
|
|
34
|
+
this.surface = surface ?? new StdoutSurface();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Frame buffering: coalesces all emit() calls until endFrame() into one
|
|
38
|
+
// surface.write, bracketed by cursor hide/show so intermediate redraw
|
|
39
|
+
// states never flicker through.
|
|
40
|
+
beginFrame(): void {
|
|
41
|
+
if (this.frameBuf === null) this.frameBuf = "\x1b[?25l";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
endFrame(): void {
|
|
45
|
+
if (this.frameBuf === null) return;
|
|
46
|
+
const out = this.frameBuf + "\x1b[?25h";
|
|
47
|
+
this.frameBuf = null;
|
|
48
|
+
this.surface.write(out);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private emit(s: string): void {
|
|
52
|
+
if (this.frameBuf !== null) this.frameBuf += s;
|
|
53
|
+
else this.surface.write(s);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private autoFrame<T>(fn: () => T): T {
|
|
57
|
+
const owned = this.frameBuf === null;
|
|
58
|
+
if (owned) this.beginFrame();
|
|
59
|
+
try { return fn(); } finally { if (owned) this.endFrame(); }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
resetCursor(): void {
|
|
63
|
+
this.cursorRowsBelow = 0;
|
|
64
|
+
this.cursorTermCol = 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
enableModeKeys(): void {
|
|
68
|
+
// Kitty progressive enhancement + bracket paste (Shift+Enter → \x1b[13;2u).
|
|
69
|
+
this.emit("\x1b[>1u\x1b[?2004h");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
disableModeKeys(): void {
|
|
73
|
+
this.emit("\x1b[<u\x1b[?2004l");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
clearPromptArea(): void {
|
|
77
|
+
this.autoFrame(() => {
|
|
78
|
+
if (this.cursorRowsBelow > 0) {
|
|
79
|
+
this.emit(`\x1b[${this.cursorRowsBelow}A`);
|
|
80
|
+
}
|
|
81
|
+
this.emit("\r\x1b[J");
|
|
82
|
+
this.cursorRowsBelow = 0;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
drawPrompt(vm: PromptVM): void {
|
|
87
|
+
this.autoFrame(() => {
|
|
88
|
+
const termW = this.surface.columns;
|
|
89
|
+
|
|
90
|
+
if (this.cursorRowsBelow > 0) {
|
|
91
|
+
this.emit(`\x1b[${this.cursorRowsBelow}A`);
|
|
92
|
+
}
|
|
93
|
+
this.emit("\r\x1b[J");
|
|
94
|
+
|
|
95
|
+
const infoPrefix = vm.agentInfo.info
|
|
96
|
+
? `${vm.agentInfo.info} ${p.success}${vm.indicator}${p.reset} `
|
|
97
|
+
: `${p.success}${vm.indicator}${p.reset} `;
|
|
98
|
+
const promptPrefix = infoPrefix + p.warning + p.bold + vm.promptIcon + " " + p.reset;
|
|
99
|
+
const promptVisLen = visibleLen(infoPrefix) + visibleLen(vm.promptIcon) + 1;
|
|
100
|
+
|
|
101
|
+
const display = vm.showBuffer ? vm.displayText : "";
|
|
102
|
+
const dCursor = vm.showBuffer ? vm.displayCursor : 0;
|
|
103
|
+
|
|
104
|
+
if (!vm.showBuffer) {
|
|
105
|
+
this.emit(promptPrefix);
|
|
106
|
+
const N = promptVisLen;
|
|
107
|
+
this.cursorRowsBelow = N > 0 ? Math.ceil(N / termW) - 1 : 0;
|
|
108
|
+
this.cursorTermCol = N === 0 ? 1 : (N % termW === 0 ? termW : (N % termW) + 1);
|
|
109
|
+
} else if (!display.includes("\n")) {
|
|
110
|
+
// DECSC/DECRC bracket the after-cursor text so the cursor lands mid-line.
|
|
111
|
+
const before = display.slice(0, dCursor);
|
|
112
|
+
const after = display.slice(dCursor);
|
|
113
|
+
this.emit(
|
|
114
|
+
promptPrefix + p.accent + before + p.reset +
|
|
115
|
+
"\x1b7" +
|
|
116
|
+
p.accent + after + p.reset +
|
|
117
|
+
"\x1b8"
|
|
118
|
+
);
|
|
119
|
+
const cursorVisCol = promptVisLen + visibleLen(before);
|
|
120
|
+
this.cursorRowsBelow = cursorVisCol > 0 ? Math.ceil(cursorVisCol / termW) - 1 : 0;
|
|
121
|
+
this.cursorTermCol = cursorVisCol === 0 ? 1 : (cursorVisCol % termW === 0 ? termW : (cursorVisCol % termW) + 1);
|
|
122
|
+
} else {
|
|
123
|
+
const lines = display.split("\n");
|
|
124
|
+
const indent = " ".repeat(promptVisLen);
|
|
125
|
+
|
|
126
|
+
let charsRemaining = dCursor;
|
|
127
|
+
let cursorLine = 0;
|
|
128
|
+
for (let li = 0; li < lines.length; li++) {
|
|
129
|
+
if (charsRemaining <= lines[li]!.length) {
|
|
130
|
+
cursorLine = li;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
charsRemaining -= lines[li]!.length + 1;
|
|
134
|
+
cursorLine = li + 1;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let output = "";
|
|
138
|
+
let cursorRowFromTop = 0;
|
|
139
|
+
let rowsSoFar = 0;
|
|
140
|
+
|
|
141
|
+
for (let li = 0; li < lines.length; li++) {
|
|
142
|
+
const prefix = li === 0 ? promptPrefix : indent;
|
|
143
|
+
const lineText = lines[li]!;
|
|
144
|
+
const lineVisLen = promptVisLen + visibleLen(lineText);
|
|
145
|
+
const lineTermRows = lineVisLen > 0 ? Math.ceil(lineVisLen / termW) : 1;
|
|
146
|
+
|
|
147
|
+
if (li === cursorLine) {
|
|
148
|
+
const before = lineText.slice(0, charsRemaining);
|
|
149
|
+
const after = lineText.slice(charsRemaining);
|
|
150
|
+
output += prefix + p.accent + before + p.reset;
|
|
151
|
+
output += "\x1b7";
|
|
152
|
+
output += p.accent + after + p.reset;
|
|
153
|
+
|
|
154
|
+
const beforeVisCol = promptVisLen + visibleLen(before);
|
|
155
|
+
cursorRowFromTop = rowsSoFar + (beforeVisCol > 0 ? Math.ceil(beforeVisCol / termW) - 1 : 0);
|
|
156
|
+
this.cursorTermCol = beforeVisCol === 0 ? 1 : (beforeVisCol % termW === 0 ? termW : (beforeVisCol % termW) + 1);
|
|
157
|
+
} else {
|
|
158
|
+
output += prefix + p.accent + lineText + p.reset;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (li < lines.length - 1) output += "\n";
|
|
162
|
+
rowsSoFar += lineTermRows;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.emit(output + "\x1b8");
|
|
166
|
+
this.cursorRowsBelow = cursorRowFromTop;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
drawAutocomplete(vm: AutocompleteVM): void {
|
|
172
|
+
if (vm.items.length === 0) return;
|
|
173
|
+
this.autoFrame(() => {
|
|
174
|
+
// Truncate descriptions so each row fits one physical line — a wrapped
|
|
175
|
+
// row makes autocompleteLines undercount, clearAutocomplete leaves a
|
|
176
|
+
// residual row, and the next drawPrompt redraws below the original
|
|
177
|
+
// prompt (staircase).
|
|
178
|
+
const termW = this.surface.columns;
|
|
179
|
+
const lines: string[] = [];
|
|
180
|
+
for (let i = 0; i < vm.items.length; i++) {
|
|
181
|
+
const item = vm.items[i]!;
|
|
182
|
+
const nameW = Math.max(12, visibleLen(item.name));
|
|
183
|
+
const overhead = 5 + nameW;
|
|
184
|
+
const descBudget = Math.max(1, termW - overhead);
|
|
185
|
+
const desc = visibleLen(item.description) > descBudget
|
|
186
|
+
? truncateToWidth(item.description, descBudget)
|
|
187
|
+
: item.description;
|
|
188
|
+
const selected = i === vm.selected;
|
|
189
|
+
if (selected) {
|
|
190
|
+
lines.push(
|
|
191
|
+
` \x1b[7m ${p.accent}${item.name.padEnd(12)}${p.reset}\x1b[7m ${desc} ${p.reset}`
|
|
192
|
+
);
|
|
193
|
+
} else {
|
|
194
|
+
lines.push(
|
|
195
|
+
` ${p.muted}${item.name.padEnd(12)} ${desc}${p.reset}`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
this.emit("\n" + lines.join("\n"));
|
|
201
|
+
this.autocompleteLines = lines.length;
|
|
202
|
+
|
|
203
|
+
if (this.autocompleteLines > 0) {
|
|
204
|
+
this.emit(`\x1b[${this.autocompleteLines}A`);
|
|
205
|
+
}
|
|
206
|
+
// Absolute column set — preceding \n may have scrolled, invalidating DECSC.
|
|
207
|
+
this.emit(`\x1b[${this.cursorTermCol}G`);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
clearAutocomplete(): void {
|
|
212
|
+
if (this.autocompleteLines <= 0) return;
|
|
213
|
+
this.autoFrame(() => {
|
|
214
|
+
// CSI B (cursor down, bounded) so we don't scroll on the last row.
|
|
215
|
+
for (let i = 0; i < this.autocompleteLines; i++) {
|
|
216
|
+
this.emit("\x1b[B\x1b[2K");
|
|
217
|
+
}
|
|
218
|
+
this.emit(`\x1b[${this.autocompleteLines}A\x1b[${this.cursorTermCol}G`);
|
|
219
|
+
this.autocompleteLines = 0;
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|