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,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool display renderer with elapsed timer and width-adaptive output.
|
|
3
|
+
*
|
|
4
|
+
* Follows the render(width) -> string[] protocol for completed tools.
|
|
5
|
+
* Also provides a spinner/timer component for in-progress tools.
|
|
6
|
+
*/
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import * as os from "node:os";
|
|
9
|
+
import { visibleLen } from "./ansi.js";
|
|
10
|
+
import { palette as p } from "./palette.js";
|
|
11
|
+
|
|
12
|
+
// ── Types ────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export type ToolDisplayMode = "full" | "compact" | "summary";
|
|
15
|
+
|
|
16
|
+
export interface ToolCallRender {
|
|
17
|
+
/** The tool title (e.g. "Read file", "Bash command"). */
|
|
18
|
+
title: string;
|
|
19
|
+
/** Optional command string for bash-like tools. */
|
|
20
|
+
command?: string;
|
|
21
|
+
/** Tool kind from ACP (read, edit, execute, search, etc.). */
|
|
22
|
+
kind?: string;
|
|
23
|
+
/** Custom icon character — when set, tool name is omitted (icon implies tool). */
|
|
24
|
+
icon?: string;
|
|
25
|
+
/** File locations affected by the tool call. */
|
|
26
|
+
locations?: { path: string; line?: number | null }[];
|
|
27
|
+
/** Raw input parameters sent to the tool. */
|
|
28
|
+
rawInput?: unknown;
|
|
29
|
+
/** Pre-formatted display detail from tool's formatCall(). Takes precedence over rawInput extraction. */
|
|
30
|
+
displayDetail?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ToolResultRender {
|
|
34
|
+
exitCode: number | null;
|
|
35
|
+
/** Output lines from the tool. */
|
|
36
|
+
outputLines?: string[];
|
|
37
|
+
/** Maximum output lines to show. Default 10. */
|
|
38
|
+
maxOutputLines?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Quiet command detection ──────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
const QUIET_PATTERNS = [
|
|
44
|
+
/^cd\b/,
|
|
45
|
+
/^mkdir\b/,
|
|
46
|
+
/^touch\b/,
|
|
47
|
+
/^rm\b/,
|
|
48
|
+
/^cp\b/,
|
|
49
|
+
/^mv\b/,
|
|
50
|
+
/^ln\b/,
|
|
51
|
+
/^chmod\b/,
|
|
52
|
+
/^chown\b/,
|
|
53
|
+
/^git\s+(add|checkout|branch|switch|stash|tag|config)\b/,
|
|
54
|
+
/^npm\s+(install|ci|uninstall)\b/,
|
|
55
|
+
/^yarn\s+(add|remove|install)\b/,
|
|
56
|
+
/^pnpm\s+(add|remove|install)\b/,
|
|
57
|
+
/^export\b/,
|
|
58
|
+
/^source\b/,
|
|
59
|
+
/^\.\s/,
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
export function isQuietCommand(command: string): boolean {
|
|
63
|
+
const trimmed = command.trim();
|
|
64
|
+
return QUIET_PATTERNS.some((p) => p.test(trimmed));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Mode selection ───────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
export function selectToolDisplayMode(width: number): ToolDisplayMode {
|
|
70
|
+
if (width >= 80) return "full";
|
|
71
|
+
if (width >= 40) return "compact";
|
|
72
|
+
return "summary";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Kind icons ──────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
const KIND_ICONS: Record<string, string> = {
|
|
78
|
+
read: "◆",
|
|
79
|
+
edit: "✎",
|
|
80
|
+
delete: "✕",
|
|
81
|
+
move: "↗",
|
|
82
|
+
search: "⌕",
|
|
83
|
+
execute: "▶",
|
|
84
|
+
think: "◇",
|
|
85
|
+
fetch: "↓",
|
|
86
|
+
switch_mode: "⇄",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
function kindIcon(kind?: string): string {
|
|
90
|
+
return kind ? (KIND_ICONS[kind] ?? "▶") : "▶";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Tool call rendering ──────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
export function renderToolCall(
|
|
96
|
+
tool: ToolCallRender,
|
|
97
|
+
width: number,
|
|
98
|
+
cwd: string = process.cwd(),
|
|
99
|
+
): string[] {
|
|
100
|
+
const mode = selectToolDisplayMode(width);
|
|
101
|
+
const icon = tool.icon ?? kindIcon(tool.kind);
|
|
102
|
+
// If the tool registered a custom icon, it's self-describing — omit the name.
|
|
103
|
+
// Otherwise, include the tool name so the user knows what ran.
|
|
104
|
+
const hasCustomIcon = !!tool.icon;
|
|
105
|
+
|
|
106
|
+
if (mode === "summary") {
|
|
107
|
+
const text = truncateVisible(`${icon} ${tool.title}`, width);
|
|
108
|
+
return [`${p.warning}${text}${p.reset}`];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const lines: string[] = [];
|
|
112
|
+
|
|
113
|
+
// Build a compact detail string to append after the title
|
|
114
|
+
let detail = "";
|
|
115
|
+
if (tool.displayDetail) {
|
|
116
|
+
detail = tool.displayDetail;
|
|
117
|
+
} else {
|
|
118
|
+
if (tool.command) {
|
|
119
|
+
detail = `$ ${tool.command}`;
|
|
120
|
+
} else if (tool.locations && tool.locations.length > 0) {
|
|
121
|
+
const loc = tool.locations.find((l) => l?.path) ?? tool.locations[0]!;
|
|
122
|
+
const lineInfo = loc.line ? `:${loc.line}` : "";
|
|
123
|
+
detail = `${shortenPath(loc.path, cwd)}${lineInfo}`;
|
|
124
|
+
} else if (tool.rawInput) {
|
|
125
|
+
const raw = tool.rawInput as Record<string, unknown>;
|
|
126
|
+
if (raw && typeof raw === "object") {
|
|
127
|
+
if (typeof raw.command === "string") {
|
|
128
|
+
detail = `$ ${raw.command}`;
|
|
129
|
+
} else if (typeof raw.source === "string") {
|
|
130
|
+
const firstLine = raw.source.split("\n").map((l) => l.trim()).find((l) => l.length > 0) ?? "";
|
|
131
|
+
detail = firstLine.length > 80 ? firstLine.slice(0, 77) + "…" : firstLine;
|
|
132
|
+
} else if (typeof raw.pattern === "string") {
|
|
133
|
+
// grep/glob — show the search pattern
|
|
134
|
+
const target = typeof raw.path === "string" ? ` ${shortenPath(raw.path, cwd)}` : "";
|
|
135
|
+
detail = `${raw.pattern}${target}`;
|
|
136
|
+
} else if (typeof raw.path === "string") {
|
|
137
|
+
// read_file, write_file, etc.
|
|
138
|
+
detail = shortenPath(raw.path, cwd);
|
|
139
|
+
} else if (typeof raw.operation === "string") {
|
|
140
|
+
detail = raw.operation;
|
|
141
|
+
if (raw.ids && Array.isArray(raw.ids)) {
|
|
142
|
+
detail += ` #${(raw.ids as number[]).join(",")}`;
|
|
143
|
+
}
|
|
144
|
+
if (typeof raw.query === "string") {
|
|
145
|
+
detail += ` "${raw.query}"`;
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
detail = formatRawInput(tool.rawInput, width - 4);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Render as single line: icon + kind + detail
|
|
155
|
+
const maxDetailW = Math.max(1, width - 4);
|
|
156
|
+
if (detail && hasCustomIcon && tool.kind) {
|
|
157
|
+
const combined = `${tool.kind} ${detail}`;
|
|
158
|
+
const truncated = combined.length > maxDetailW ? combined.slice(0, maxDetailW - 1) + "…" : combined;
|
|
159
|
+
lines.push(`${p.warning}${icon}${p.reset} ${p.dim}${truncated}${p.reset}`);
|
|
160
|
+
} else if (detail && hasCustomIcon) {
|
|
161
|
+
if (detail.length > maxDetailW) detail = detail.slice(0, maxDetailW - 1) + "…";
|
|
162
|
+
lines.push(`${p.warning}${icon}${p.reset} ${p.dim}${detail}${p.reset}`);
|
|
163
|
+
} else if (detail) {
|
|
164
|
+
const prefix = `${tool.title}: `;
|
|
165
|
+
const combined = prefix + detail;
|
|
166
|
+
const truncated = combined.length > maxDetailW ? combined.slice(0, maxDetailW - 1) + "…" : combined;
|
|
167
|
+
lines.push(`${p.warning}${icon}${p.reset} ${p.dim}${truncated}${p.reset}`);
|
|
168
|
+
} else {
|
|
169
|
+
lines.push(`${p.warning}${icon} ${tool.title}${p.reset}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Show additional file locations on separate lines (if more than one)
|
|
173
|
+
if (mode === "full" && tool.locations && tool.locations.length > 1) {
|
|
174
|
+
for (const loc of tool.locations.slice(1)) {
|
|
175
|
+
const lineInfo = loc.line ? `:${loc.line}` : "";
|
|
176
|
+
lines.push(` ${p.dim}${shortenPath(loc.path, cwd)}${lineInfo}${p.reset}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return lines;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Format raw input parameters into a compact single-line summary.
|
|
185
|
+
*/
|
|
186
|
+
function formatRawInput(raw: unknown, maxWidth: number): string {
|
|
187
|
+
if (raw == null) return "";
|
|
188
|
+
if (typeof raw === "string") {
|
|
189
|
+
return raw.length > maxWidth ? raw.slice(0, maxWidth - 1) + "…" : raw;
|
|
190
|
+
}
|
|
191
|
+
if (typeof raw !== "object") return String(raw);
|
|
192
|
+
|
|
193
|
+
// Show key=value pairs for objects
|
|
194
|
+
const obj = raw as Record<string, unknown>;
|
|
195
|
+
const parts: string[] = [];
|
|
196
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
197
|
+
if (val == null) continue;
|
|
198
|
+
const valStr = typeof val === "string" ? val : JSON.stringify(val);
|
|
199
|
+
parts.push(`${key}=${valStr}`);
|
|
200
|
+
}
|
|
201
|
+
const joined = parts.join(" ");
|
|
202
|
+
return joined.length > maxWidth ? joined.slice(0, maxWidth - 1) + "…" : joined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── Tool result rendering ────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
export function renderToolResult(
|
|
208
|
+
result: ToolResultRender,
|
|
209
|
+
width: number,
|
|
210
|
+
): string[] {
|
|
211
|
+
const mode = selectToolDisplayMode(width);
|
|
212
|
+
const lines: string[] = [];
|
|
213
|
+
|
|
214
|
+
// Status indicator
|
|
215
|
+
if (result.exitCode === null) {
|
|
216
|
+
lines.push(` ${p.muted}(timed out)${p.reset}`);
|
|
217
|
+
} else if (result.exitCode === 0) {
|
|
218
|
+
lines.push(` ${p.success}✓${p.reset}`);
|
|
219
|
+
} else {
|
|
220
|
+
lines.push(` ${p.error}✗ exit ${result.exitCode}${p.reset}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Output preview (full mode only)
|
|
224
|
+
if (mode === "full" && result.outputLines && result.outputLines.length > 0) {
|
|
225
|
+
const maxLines = result.maxOutputLines ?? 10;
|
|
226
|
+
const total = result.outputLines.length;
|
|
227
|
+
const shown = result.outputLines.slice(0, maxLines);
|
|
228
|
+
const maxTextW = Math.max(1, width - 6);
|
|
229
|
+
|
|
230
|
+
for (const line of shown) {
|
|
231
|
+
const text = line.length > maxTextW
|
|
232
|
+
? line.slice(0, maxTextW - 1) + "…"
|
|
233
|
+
: line;
|
|
234
|
+
lines.push(` ${p.dim} ${text}${p.reset}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (total > maxLines) {
|
|
238
|
+
lines.push(` ${p.dim} … ${total - maxLines} more lines${p.reset}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return lines;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── Elapsed timer ────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
export function formatElapsed(ms: number): string {
|
|
248
|
+
if (ms < 1000) return "";
|
|
249
|
+
const s = Math.floor(ms / 1000);
|
|
250
|
+
if (s < 60) return `${s}s`;
|
|
251
|
+
const m = Math.floor(s / 60);
|
|
252
|
+
const rs = s % 60;
|
|
253
|
+
if (m < 60) return rs > 0 ? `${m}m ${rs}s` : `${m}m`;
|
|
254
|
+
const h = Math.floor(m / 60);
|
|
255
|
+
const rm = m % 60;
|
|
256
|
+
return rm > 0 ? `${h}h ${rm}m` : `${h}h`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── Spinner with elapsed timer ───────────────────────────────────
|
|
260
|
+
|
|
261
|
+
export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
262
|
+
|
|
263
|
+
export interface SpinnerState {
|
|
264
|
+
frame: number;
|
|
265
|
+
startTime: number;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export interface SpinnerOpts {
|
|
269
|
+
color?: string;
|
|
270
|
+
hint?: string;
|
|
271
|
+
startTime?: number;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function createSpinner(opts?: { startTime?: number }): SpinnerState {
|
|
275
|
+
return { frame: 0, startTime: opts?.startTime || Date.now() };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Pure function: render the current spinner line and advance the frame.
|
|
280
|
+
* Does not write to stdout — the caller is responsible for output.
|
|
281
|
+
*/
|
|
282
|
+
export function renderSpinnerLine(
|
|
283
|
+
state: SpinnerState,
|
|
284
|
+
label: string,
|
|
285
|
+
opts?: SpinnerOpts,
|
|
286
|
+
): string {
|
|
287
|
+
const frame = SPINNER_FRAMES[state.frame % SPINNER_FRAMES.length];
|
|
288
|
+
state.frame++;
|
|
289
|
+
const color = opts?.color ?? p.accent;
|
|
290
|
+
const elapsed = formatElapsed(Date.now() - state.startTime);
|
|
291
|
+
const timer = elapsed ? ` ${p.dim}${elapsed}${p.reset}` : "";
|
|
292
|
+
const hint = opts?.hint ? ` ${p.dim}${opts.hint}${p.reset}` : "";
|
|
293
|
+
return `${color}${frame} ${label}...${p.reset}${timer}${hint}`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ── Helpers ──────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Shorten an absolute path to a relative or tilde-prefixed form.
|
|
300
|
+
*/
|
|
301
|
+
function shortenPath(p: string | undefined | null, cwd: string): string {
|
|
302
|
+
if (!p || typeof p !== "string") return "";
|
|
303
|
+
if (p.startsWith(cwd + "/")) return p.slice(cwd.length + 1);
|
|
304
|
+
if (p.startsWith(cwd)) return p.slice(cwd.length) || ".";
|
|
305
|
+
const home = process.env.HOME ?? os.homedir();
|
|
306
|
+
if (home && p.startsWith(home + "/")) return "~/" + p.slice(home.length + 1);
|
|
307
|
+
return p;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function truncateVisible(text: string, maxWidth: number): string {
|
|
311
|
+
if (visibleLen(text) <= maxWidth) return text;
|
|
312
|
+
// Simple truncation for plain text (no ANSI)
|
|
313
|
+
if (maxWidth <= 1) return "…";
|
|
314
|
+
return text.slice(0, maxWidth - 1) + "…";
|
|
315
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive UI primitive for tools.
|
|
3
|
+
*
|
|
4
|
+
* Gives a tool imperative control over rendering and input on the active
|
|
5
|
+
* surface. The tool provides render() + handleInput(), the primitive
|
|
6
|
+
* handles surface writing, input interception, shell pause/unpause,
|
|
7
|
+
* and cleanup.
|
|
8
|
+
*/
|
|
9
|
+
import type { EventBus } from "../core/event-bus.js";
|
|
10
|
+
import type { RenderSurface } from "./compositor.js";
|
|
11
|
+
import type { InteractiveSession, ToolUI } from "../agent/types.js";
|
|
12
|
+
|
|
13
|
+
/** Clear N lines above the cursor. */
|
|
14
|
+
function clearLines(surface: RenderSurface, count: number): void {
|
|
15
|
+
for (let i = 0; i < count; i++) {
|
|
16
|
+
surface.write("\x1b[A\x1b[2K");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createToolUI(
|
|
21
|
+
bus: EventBus,
|
|
22
|
+
surface: RenderSurface,
|
|
23
|
+
): ToolUI {
|
|
24
|
+
return {
|
|
25
|
+
custom<T>(session: InteractiveSession<T>): Promise<T> {
|
|
26
|
+
return new Promise<T>((resolve) => {
|
|
27
|
+
let prevLineCount = 0;
|
|
28
|
+
let finished = false;
|
|
29
|
+
|
|
30
|
+
const done = (result: T) => {
|
|
31
|
+
if (finished) return;
|
|
32
|
+
finished = true;
|
|
33
|
+
bus.offPipe("input:intercept", interceptor);
|
|
34
|
+
bus.emit("shell:stdout-hide", {});
|
|
35
|
+
bus.emit("tool:interactive-end", {});
|
|
36
|
+
session.onUnmount?.();
|
|
37
|
+
resolve(result);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const render = () => {
|
|
41
|
+
if (finished) return;
|
|
42
|
+
clearLines(surface, prevLineCount);
|
|
43
|
+
const lines = session.render(surface.columns);
|
|
44
|
+
for (const line of lines) {
|
|
45
|
+
surface.writeLine(line);
|
|
46
|
+
}
|
|
47
|
+
prevLineCount = lines.length;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const interceptor = (payload: { data: string; consumed: boolean }) => {
|
|
51
|
+
if (finished) return payload;
|
|
52
|
+
// Let Ctrl+C through for agent cancellation
|
|
53
|
+
if (payload.data === "\x03") return payload;
|
|
54
|
+
session.handleInput(payload.data, done);
|
|
55
|
+
render();
|
|
56
|
+
return { ...payload, consumed: true };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Setup
|
|
60
|
+
bus.emit("tool:interactive-start", {});
|
|
61
|
+
bus.emit("shell:stdout-show", {});
|
|
62
|
+
bus.onPipe("input:intercept", interceptor);
|
|
63
|
+
// Drop to a fresh row in case the cursor was mid-line; uncounted
|
|
64
|
+
// so clearLines on dismiss stops at the gap, not above it.
|
|
65
|
+
surface.write("\n");
|
|
66
|
+
session.onMount?.(() => render(), done);
|
|
67
|
+
render();
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
package/src/utils/tty.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* libuv's setRawMode(true) keeps OPOST on, so the kernel rewrites \n to
|
|
5
|
+
* \r\n. TUIs over ssh (e.g. emacs -nw) emit relative moves like "\n\n\n\b\b"
|
|
6
|
+
* assuming a raw path — with OPOST on, \n snaps the cursor to col 0 and
|
|
7
|
+
* subsequent text lands in the wrong column. Call after every setRawMode(true).
|
|
8
|
+
*/
|
|
9
|
+
export function clearOpost(): void {
|
|
10
|
+
if (!process.stdin.isTTY) return;
|
|
11
|
+
try {
|
|
12
|
+
execSync("stty -opost", { stdio: "inherit" });
|
|
13
|
+
} catch { /* best effort */ }
|
|
14
|
+
}
|