@phren/agent 0.1.2 → 0.1.4
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-loop/index.js +214 -0
- package/dist/agent-loop/stream.js +124 -0
- package/dist/agent-loop/types.js +13 -0
- package/dist/agent-loop.js +7 -326
- package/dist/commands/info.js +146 -0
- package/dist/commands/memory.js +165 -0
- package/dist/commands/model.js +138 -0
- package/dist/commands/session.js +213 -0
- package/dist/commands.js +25 -297
- package/dist/config.js +6 -2
- package/dist/index.js +10 -4
- package/dist/mcp-client.js +11 -7
- package/dist/multi/multi-commands.js +170 -0
- package/dist/multi/multi-events.js +81 -0
- package/dist/multi/multi-render.js +146 -0
- package/dist/multi/pane.js +28 -0
- package/dist/multi/spawner.js +3 -2
- package/dist/multi/tui-multi.js +39 -454
- package/dist/permissions/allowlist.js +2 -2
- package/dist/permissions/shell-safety.js +8 -0
- package/dist/providers/anthropic.js +72 -33
- package/dist/providers/codex.js +121 -60
- package/dist/providers/openai-compat.js +6 -1
- package/dist/repl.js +2 -2
- package/dist/system-prompt.js +24 -26
- package/dist/tools/glob.js +30 -6
- package/dist/tools/shell.js +5 -2
- package/dist/tui/ansi.js +48 -0
- package/dist/tui/components/AgentMessage.js +5 -0
- package/dist/tui/components/App.js +70 -0
- package/dist/tui/components/Banner.js +44 -0
- package/dist/tui/components/ChatMessage.js +23 -0
- package/dist/tui/components/InputArea.js +23 -0
- package/dist/tui/components/Separator.js +7 -0
- package/dist/tui/components/StatusBar.js +25 -0
- package/dist/tui/components/SteerQueue.js +7 -0
- package/dist/tui/components/StreamingText.js +5 -0
- package/dist/tui/components/ThinkingIndicator.js +20 -0
- package/dist/tui/components/ToolCall.js +11 -0
- package/dist/tui/components/UserMessage.js +5 -0
- package/dist/tui/hooks/useKeyboardShortcuts.js +89 -0
- package/dist/tui/hooks/useSlashCommands.js +52 -0
- package/dist/tui/index.js +5 -0
- package/dist/tui/ink-entry.js +271 -0
- package/dist/tui/menu-mode.js +86 -0
- package/dist/tui/tool-render.js +43 -0
- package/dist/tui.js +378 -252
- package/package.json +9 -2
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useCallback } from "react";
|
|
3
|
+
import { Static, Box, Text, useApp } from "ink";
|
|
4
|
+
import { Banner } from "./Banner.js";
|
|
5
|
+
import { UserMessage } from "./UserMessage.js";
|
|
6
|
+
import { StreamingText } from "./StreamingText.js";
|
|
7
|
+
import { ToolCall } from "./ToolCall.js";
|
|
8
|
+
import { ThinkingIndicator } from "./ThinkingIndicator.js";
|
|
9
|
+
import { SteerQueue } from "./SteerQueue.js";
|
|
10
|
+
import { InputArea, PermissionsLine } from "./InputArea.js";
|
|
11
|
+
import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts.js";
|
|
12
|
+
function CompletedItem({ msg }) {
|
|
13
|
+
switch (msg.kind) {
|
|
14
|
+
case "user":
|
|
15
|
+
return _jsx(UserMessage, { text: msg.text });
|
|
16
|
+
case "assistant":
|
|
17
|
+
return (_jsxs(Box, { flexDirection: "column", children: [msg.text ? _jsx(StreamingText, { text: msg.text }) : null, msg.toolCalls?.map((tc, i) => (_jsx(ToolCall, { ...tc }, i)))] }));
|
|
18
|
+
case "status":
|
|
19
|
+
return _jsx(Text, { dimColor: true, children: msg.text });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function App({ state, completedMessages, streamingText, completedToolCalls, thinking, thinkStartTime, thinkElapsed, steerQueue, running, showBanner, inputHistory, onSubmit, onPermissionCycle, onCancelTurn, onExit, }) {
|
|
23
|
+
const { exit } = useApp();
|
|
24
|
+
const [inputValue, setInputValue] = useState("");
|
|
25
|
+
const [bashMode, setBashMode] = useState(false);
|
|
26
|
+
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
27
|
+
const [ctrlCCount, setCtrlCCount] = useState(0);
|
|
28
|
+
const handleSubmit = useCallback((value) => {
|
|
29
|
+
setInputValue("");
|
|
30
|
+
setHistoryIndex(-1);
|
|
31
|
+
if (value === "")
|
|
32
|
+
return;
|
|
33
|
+
// ! at start of empty input toggles bash mode
|
|
34
|
+
if (value === "!" && !bashMode) {
|
|
35
|
+
setBashMode(true);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
onSubmit(bashMode ? `!${value}` : value);
|
|
39
|
+
setBashMode(false);
|
|
40
|
+
}, [bashMode, onSubmit]);
|
|
41
|
+
useKeyboardShortcuts({
|
|
42
|
+
isRunning: running,
|
|
43
|
+
inputValue,
|
|
44
|
+
bashMode,
|
|
45
|
+
inputHistory,
|
|
46
|
+
historyIndex,
|
|
47
|
+
ctrlCCount,
|
|
48
|
+
onSetInput: setInputValue,
|
|
49
|
+
onSetBashMode: setBashMode,
|
|
50
|
+
onSetHistoryIndex: setHistoryIndex,
|
|
51
|
+
onSetCtrlCCount: setCtrlCCount,
|
|
52
|
+
onExit: () => { onExit(); exit(); },
|
|
53
|
+
onCyclePermissions: onPermissionCycle,
|
|
54
|
+
onCancelTurn,
|
|
55
|
+
});
|
|
56
|
+
// Build the Static items — banner first if shown, then completed messages
|
|
57
|
+
const staticItems = [];
|
|
58
|
+
if (showBanner) {
|
|
59
|
+
staticItems.push({ id: "banner", kind: "banner" });
|
|
60
|
+
}
|
|
61
|
+
for (const msg of completedMessages) {
|
|
62
|
+
staticItems.push(msg);
|
|
63
|
+
}
|
|
64
|
+
return (_jsxs(_Fragment, { children: [_jsx(Static, { items: staticItems, children: (item) => {
|
|
65
|
+
if (item.kind === "banner") {
|
|
66
|
+
return (_jsx(Box, { children: _jsx(Banner, { version: state.version }) }, "banner"));
|
|
67
|
+
}
|
|
68
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(CompletedItem, { msg: item }) }, item.id));
|
|
69
|
+
} }), _jsxs(Box, { flexDirection: "column", children: [completedToolCalls.map((tc, i) => (_jsx(ToolCall, { ...tc }, `tc-${i}`))), streamingText !== "" && _jsx(StreamingText, { text: streamingText }), thinking && _jsx(ThinkingIndicator, { startTime: thinkStartTime }), thinkElapsed !== null && (_jsxs(Text, { dimColor: true, children: [" ", "\u25c6", " thought for ", thinkElapsed, "s"] })), ctrlCCount > 0 && !running && (_jsxs(Text, { dimColor: true, children: [" ", "Press Ctrl+C again to exit."] })), _jsx(SteerQueue, { items: steerQueue }), _jsx(InputArea, { value: inputValue, onChange: setInputValue, onSubmit: handleSubmit, bashMode: bashMode, focus: !running }), _jsx(PermissionsLine, { mode: state.permMode })] })] }));
|
|
70
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
const _require = createRequire(import.meta.url);
|
|
6
|
+
let cachedArt = null;
|
|
7
|
+
let artLoaded = false;
|
|
8
|
+
function getArtLines() {
|
|
9
|
+
if (artLoaded)
|
|
10
|
+
return cachedArt ?? [];
|
|
11
|
+
artLoaded = true;
|
|
12
|
+
try {
|
|
13
|
+
const mod = _require("@phren/cli/phren-art");
|
|
14
|
+
cachedArt = mod.PHREN_ART.filter((l) => l.trim());
|
|
15
|
+
return cachedArt;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function stripAnsi(t) {
|
|
22
|
+
return t.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
|
|
23
|
+
}
|
|
24
|
+
export function Banner({ version }) {
|
|
25
|
+
const cwd = process.cwd().replace(os.homedir(), "~");
|
|
26
|
+
const artLines = getArtLines();
|
|
27
|
+
const maxArtWidth = 26;
|
|
28
|
+
const infoLines = [
|
|
29
|
+
{ key: "title", node: _jsx(Text, { bold: true, color: "magenta", children: "◆ phren" }) },
|
|
30
|
+
{ key: "version", node: _jsx(Text, { dimColor: true, children: " v" + version }) },
|
|
31
|
+
{ key: "cwd", node: _jsx(Text, { dimColor: true, children: cwd }) },
|
|
32
|
+
];
|
|
33
|
+
if (artLines.length > 0) {
|
|
34
|
+
const rowCount = Math.max(artLines.length, infoLines.length);
|
|
35
|
+
return (_jsxs(Box, { flexDirection: "column", children: [Array.from({ length: rowCount }, (_, i) => {
|
|
36
|
+
const artLine = i < artLines.length ? artLines[i] : "";
|
|
37
|
+
const artVisible = stripAnsi(artLine).length;
|
|
38
|
+
const padding = Math.max(0, maxArtWidth - artVisible);
|
|
39
|
+
const info = i < infoLines.length ? infoLines[i] : null;
|
|
40
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { children: artLine + " ".repeat(padding) }), info ? info.node : _jsx(Text, { children: "" })] }, i));
|
|
41
|
+
}), _jsx(Text, { children: "" })] }));
|
|
42
|
+
}
|
|
43
|
+
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [_jsxs(Box, { children: [infoLines[0].node, infoLines[1].node] }), infoLines[2].node, _jsx(Text, { children: "" })] }));
|
|
44
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { formatToolInput, formatDuration } from "../tool-render.js";
|
|
4
|
+
const COMPACT_LINES = 3;
|
|
5
|
+
export function ChatMessage({ role, text, toolCalls }) {
|
|
6
|
+
if (role === "user") {
|
|
7
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, children: [_jsx(Text, { bold: true, children: "You: " }), _jsx(Text, { children: text })] }));
|
|
8
|
+
}
|
|
9
|
+
if (role === "system") {
|
|
10
|
+
return (_jsx(Box, { marginBottom: 0, children: _jsx(Text, { dimColor: true, children: text }) }));
|
|
11
|
+
}
|
|
12
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 0, children: [_jsx(Text, { children: text }), toolCalls?.map((tc, i) => (_jsx(ToolCallLine, { ...tc }, i)))] }));
|
|
13
|
+
}
|
|
14
|
+
function ToolCallLine({ name, input, output, isError, durationMs }) {
|
|
15
|
+
const preview = formatToolInput(name, input);
|
|
16
|
+
const dur = formatDuration(durationMs);
|
|
17
|
+
const icon = isError ? "✗" : "→";
|
|
18
|
+
const iconColor = isError ? "red" : "green";
|
|
19
|
+
const allLines = output.split("\n").filter(Boolean);
|
|
20
|
+
const shown = allLines.slice(0, COMPACT_LINES);
|
|
21
|
+
const overflow = allLines.length - COMPACT_LINES;
|
|
22
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [" ", _jsx(Text, { color: iconColor, children: icon }), " ", _jsx(Text, { bold: true, children: name }), " ", _jsx(Text, { color: "gray", children: preview }), " ", _jsx(Text, { dimColor: true, children: dur })] }), shown.map((line, j) => (_jsxs(Text, { dimColor: true, children: [" ", line.slice(0, (process.stdout.columns || 80) - 6)] }, j))), overflow > 0 && _jsxs(Text, { dimColor: true, children: [" ", "... +", overflow, " lines"] })] }));
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useStdout } from "ink";
|
|
3
|
+
import TextInput from "ink-text-input";
|
|
4
|
+
import { PERMISSION_LABELS, PERMISSION_ICONS } from "../ansi.js";
|
|
5
|
+
export function InputArea({ value, onChange, onSubmit, bashMode, focus }) {
|
|
6
|
+
const { stdout } = useStdout();
|
|
7
|
+
const columns = stdout?.columns || 80;
|
|
8
|
+
const sep = "\u2500".repeat(columns);
|
|
9
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: sep }), _jsxs(Box, { children: [bashMode
|
|
10
|
+
? _jsx(Text, { color: "yellow", children: "! " })
|
|
11
|
+
: _jsxs(Text, { dimColor: true, children: ["\u25b8", " "] }), _jsx(TextInput, { value: value, onChange: onChange, onSubmit: onSubmit, focus: focus, showCursor: true })] }), _jsx(Text, { dimColor: true, children: sep })] }));
|
|
12
|
+
}
|
|
13
|
+
const PERM_COLOR_MAP = {
|
|
14
|
+
"suggest": "cyan",
|
|
15
|
+
"auto-confirm": "green",
|
|
16
|
+
"full-auto": "yellow",
|
|
17
|
+
};
|
|
18
|
+
export function PermissionsLine({ mode }) {
|
|
19
|
+
const icon = PERMISSION_ICONS[mode];
|
|
20
|
+
const label = PERMISSION_LABELS[mode];
|
|
21
|
+
const color = PERM_COLOR_MAP[mode];
|
|
22
|
+
return (_jsxs(Text, { children: [" ", _jsxs(Text, { color: color, children: [icon, " ", label, " permissions"] }), " ", _jsxs(Text, { dimColor: true, children: ["(shift+tab toggle ", "\u00b7", " esc to interrupt)"] })] }));
|
|
23
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Text, useStdout } from "ink";
|
|
3
|
+
export function Separator() {
|
|
4
|
+
const { stdout } = useStdout();
|
|
5
|
+
const columns = stdout?.columns || 80;
|
|
6
|
+
return _jsx(Text, { dimColor: true, children: "\u2500".repeat(columns) });
|
|
7
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Text, useStdout } from "ink";
|
|
3
|
+
import { PERMISSION_LABELS } from "../ansi.js";
|
|
4
|
+
export function StatusBar({ provider, project, turns, cost, permMode, agentCount }) {
|
|
5
|
+
const { stdout } = useStdout();
|
|
6
|
+
const width = stdout?.columns || 80;
|
|
7
|
+
const modeLabel = permMode ? PERMISSION_LABELS[permMode] : "";
|
|
8
|
+
const agentTag = agentCount && agentCount > 0 ? `A${agentCount}` : "";
|
|
9
|
+
const leftParts = [" \u25c6 phren", provider];
|
|
10
|
+
if (project)
|
|
11
|
+
leftParts.push(project);
|
|
12
|
+
const left = leftParts.join(" \u00b7 ");
|
|
13
|
+
const rightParts = [];
|
|
14
|
+
if (modeLabel)
|
|
15
|
+
rightParts.push(modeLabel);
|
|
16
|
+
if (agentTag)
|
|
17
|
+
rightParts.push(agentTag);
|
|
18
|
+
if (cost)
|
|
19
|
+
rightParts.push(cost);
|
|
20
|
+
rightParts.push(`T${turns}`);
|
|
21
|
+
const right = rightParts.join(" ") + " ";
|
|
22
|
+
const pad = Math.max(0, width - left.length - right.length);
|
|
23
|
+
const fullLine = left + " ".repeat(pad) + right;
|
|
24
|
+
return _jsx(Text, { inverse: true, children: fullLine });
|
|
25
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
export function SteerQueue({ items }) {
|
|
4
|
+
if (items.length === 0)
|
|
5
|
+
return null;
|
|
6
|
+
return (_jsx(Box, { flexDirection: "column", children: items.map((item, i) => (_jsxs(Text, { color: "yellow", children: [" ", "\u21B3 steer: ", item.slice(0, 60)] }, i))) }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from "react";
|
|
3
|
+
import { Text } from "ink";
|
|
4
|
+
export function ThinkingIndicator({ startTime }) {
|
|
5
|
+
const [frame, setFrame] = useState(0);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const timer = setInterval(() => {
|
|
8
|
+
setFrame((f) => f + 1);
|
|
9
|
+
}, 50);
|
|
10
|
+
return () => clearInterval(timer);
|
|
11
|
+
}, []);
|
|
12
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
13
|
+
// Gentle sine-wave interpolation between phren purple and cyan
|
|
14
|
+
const t = (Math.sin(frame * 0.08) + 1) / 2;
|
|
15
|
+
const r = Math.round(155 * (1 - t) + 40 * t);
|
|
16
|
+
const g = Math.round(140 * (1 - t) + 211 * t);
|
|
17
|
+
const b = Math.round(250 * (1 - t) + 242 * t);
|
|
18
|
+
const hex = `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
19
|
+
return (_jsxs(Text, { children: [" ", _jsx(Text, { color: hex, children: "\u25C6 thinking" }), " ", _jsxs(Text, { dimColor: true, children: [elapsed, "s"] })] }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { formatToolInput, formatDuration, COMPACT_LINES } from "../tool-render.js";
|
|
4
|
+
export function ToolCall({ name, input, output, isError, durationMs }) {
|
|
5
|
+
const preview = formatToolInput(name, input);
|
|
6
|
+
const dur = formatDuration(durationMs);
|
|
7
|
+
const allLines = output.split("\n").filter(Boolean);
|
|
8
|
+
const shown = allLines.slice(0, COMPACT_LINES);
|
|
9
|
+
const overflow = allLines.length - COMPACT_LINES;
|
|
10
|
+
return (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [_jsxs(Box, { children: [_jsxs(Text, { color: isError ? "red" : "green", children: [isError ? "\u2717" : "\u2192", " "] }), _jsx(Text, { bold: true, children: name }), _jsxs(Text, { color: "gray", children: [" ", preview] }), _jsxs(Text, { dimColor: true, children: [" ", dur] })] }), shown.map((line, i) => (_jsx(Text, { dimColor: true, children: " " + line.slice(0, 120) }, i))), overflow > 0 && (_jsx(Text, { dimColor: true, children: " ... +" + overflow + " lines" }))] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
export function UserMessage({ text }) {
|
|
4
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: ["❯", " ", text] }), _jsx(Text, { children: "" })] }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useInput } from "ink";
|
|
2
|
+
const SLASH_COMMANDS = [
|
|
3
|
+
"/help", "/turns", "/clear", "/cwd", "/files", "/cost", "/plan", "/undo",
|
|
4
|
+
"/context", "/model", "/provider", "/preset", "/session", "/history",
|
|
5
|
+
"/compact", "/diff", "/git", "/mem", "/ask", "/spawn", "/agents",
|
|
6
|
+
"/mode", "/exit", "/quit",
|
|
7
|
+
];
|
|
8
|
+
export function useKeyboardShortcuts(opts) {
|
|
9
|
+
useInput((input, key) => {
|
|
10
|
+
// Reset Ctrl+C count on any non-Ctrl+C keypress
|
|
11
|
+
if (!(input === "c" && key.ctrl)) {
|
|
12
|
+
if (opts.ctrlCCount > 0)
|
|
13
|
+
opts.onSetCtrlCCount(0);
|
|
14
|
+
}
|
|
15
|
+
// Ctrl+D -- exit cleanly
|
|
16
|
+
if (key.ctrl && input === "d") {
|
|
17
|
+
opts.onExit();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
// Shift+Tab -- cycle permission mode
|
|
21
|
+
if (key.shift && key.tab) {
|
|
22
|
+
opts.onCyclePermissions();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
// Ctrl+C -- progressive: cancel turn / clear input / warn / quit
|
|
26
|
+
if (key.ctrl && input === "c") {
|
|
27
|
+
if (opts.isRunning) {
|
|
28
|
+
opts.onCancelTurn();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (opts.inputValue) {
|
|
32
|
+
opts.onSetInput("");
|
|
33
|
+
opts.onSetCtrlCCount(0);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (opts.ctrlCCount >= 1) {
|
|
37
|
+
opts.onExit();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
opts.onSetCtrlCCount(opts.ctrlCCount + 1);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Escape -- exit bash mode or clear input
|
|
44
|
+
if (key.escape) {
|
|
45
|
+
if (opts.bashMode) {
|
|
46
|
+
opts.onSetBashMode(false);
|
|
47
|
+
opts.onSetInput("");
|
|
48
|
+
}
|
|
49
|
+
else if (opts.inputValue) {
|
|
50
|
+
opts.onSetInput("");
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Up arrow -- recall older history
|
|
55
|
+
if (key.upArrow && !opts.isRunning) {
|
|
56
|
+
const history = opts.inputHistory;
|
|
57
|
+
if (history.length === 0)
|
|
58
|
+
return;
|
|
59
|
+
const newIndex = Math.min(opts.historyIndex + 1, history.length - 1);
|
|
60
|
+
opts.onSetHistoryIndex(newIndex);
|
|
61
|
+
opts.onSetInput(history[history.length - 1 - newIndex]);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Down arrow -- recall newer history
|
|
65
|
+
if (key.downArrow && !opts.isRunning) {
|
|
66
|
+
const history = opts.inputHistory;
|
|
67
|
+
if (opts.historyIndex <= 0) {
|
|
68
|
+
opts.onSetHistoryIndex(-1);
|
|
69
|
+
opts.onSetInput("");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const newIndex = opts.historyIndex - 1;
|
|
73
|
+
opts.onSetHistoryIndex(newIndex);
|
|
74
|
+
opts.onSetInput(history[history.length - 1 - newIndex]);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// Tab -- slash command completion when input starts with /
|
|
78
|
+
if (key.tab && !key.shift && !opts.isRunning) {
|
|
79
|
+
const val = opts.inputValue;
|
|
80
|
+
if (val.startsWith("/") && val.length > 1) {
|
|
81
|
+
const matches = SLASH_COMMANDS.filter(c => c.startsWith(val));
|
|
82
|
+
if (matches.length === 1) {
|
|
83
|
+
opts.onSetInput(matches[0]);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slash command handling for the Ink TUI.
|
|
3
|
+
* Captures stderr output from handleCommand and returns it as display text.
|
|
4
|
+
*/
|
|
5
|
+
import { handleCommand } from "../../commands.js";
|
|
6
|
+
export function useSlashCommands(opts) {
|
|
7
|
+
return {
|
|
8
|
+
/** Try to handle input as a slash command. Returns true if handled. */
|
|
9
|
+
tryHandleCommand(input) {
|
|
10
|
+
if (!input.startsWith("/"))
|
|
11
|
+
return false;
|
|
12
|
+
// Capture stderr writes during command execution
|
|
13
|
+
const captured = [];
|
|
14
|
+
const origWrite = process.stderr.write;
|
|
15
|
+
process.stderr.write = ((chunk) => {
|
|
16
|
+
captured.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString());
|
|
17
|
+
return true;
|
|
18
|
+
});
|
|
19
|
+
try {
|
|
20
|
+
const result = handleCommand(input, opts.commandContext);
|
|
21
|
+
if (result instanceof Promise) {
|
|
22
|
+
// Async command — restore stderr, then capture async output
|
|
23
|
+
process.stderr.write = origWrite;
|
|
24
|
+
result.then(() => {
|
|
25
|
+
// For async commands, stderr was already restored so output went to real stderr.
|
|
26
|
+
// Future improvement: capture async output too.
|
|
27
|
+
});
|
|
28
|
+
flush();
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
if (result === true) {
|
|
32
|
+
flush();
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
// Not a recognized command (result === false)
|
|
36
|
+
flush();
|
|
37
|
+
return true; // Still starts with /, so don't send to agent
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
process.stderr.write = origWrite;
|
|
41
|
+
}
|
|
42
|
+
function flush() {
|
|
43
|
+
if (captured.length > 0) {
|
|
44
|
+
// Strip ANSI dim/reset codes for cleaner display in Ink
|
|
45
|
+
const text = captured.join("").replace(/\n$/, "");
|
|
46
|
+
if (text)
|
|
47
|
+
opts.onOutput(text);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { ESC, s, cols, stripAnsi } from "./ansi.js";
|
|
2
|
+
export { PERMISSION_MODES, nextPermissionMode, PERMISSION_LABELS, PERMISSION_ICONS, PERMISSION_COLORS, permTag } from "./ansi.js";
|
|
3
|
+
export { COMPACT_LINES, formatDuration, formatToolInput, renderToolCall } from "./tool-render.js";
|
|
4
|
+
export { loadMenuModule, renderMenu, enterMenuMode, exitMenuMode, handleMenuKeypress } from "./menu-mode.js";
|
|
5
|
+
export { startInkTui } from "./ink-entry.js";
|