@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.
Files changed (48) hide show
  1. package/dist/agent-loop/index.js +214 -0
  2. package/dist/agent-loop/stream.js +124 -0
  3. package/dist/agent-loop/types.js +13 -0
  4. package/dist/agent-loop.js +7 -326
  5. package/dist/commands/info.js +146 -0
  6. package/dist/commands/memory.js +165 -0
  7. package/dist/commands/model.js +138 -0
  8. package/dist/commands/session.js +213 -0
  9. package/dist/commands.js +25 -297
  10. package/dist/config.js +6 -2
  11. package/dist/index.js +10 -4
  12. package/dist/mcp-client.js +11 -7
  13. package/dist/multi/multi-commands.js +170 -0
  14. package/dist/multi/multi-events.js +81 -0
  15. package/dist/multi/multi-render.js +146 -0
  16. package/dist/multi/pane.js +28 -0
  17. package/dist/multi/spawner.js +3 -2
  18. package/dist/multi/tui-multi.js +39 -454
  19. package/dist/permissions/allowlist.js +2 -2
  20. package/dist/permissions/shell-safety.js +8 -0
  21. package/dist/providers/anthropic.js +72 -33
  22. package/dist/providers/codex.js +121 -60
  23. package/dist/providers/openai-compat.js +6 -1
  24. package/dist/repl.js +2 -2
  25. package/dist/system-prompt.js +24 -26
  26. package/dist/tools/glob.js +30 -6
  27. package/dist/tools/shell.js +5 -2
  28. package/dist/tui/ansi.js +48 -0
  29. package/dist/tui/components/AgentMessage.js +5 -0
  30. package/dist/tui/components/App.js +70 -0
  31. package/dist/tui/components/Banner.js +44 -0
  32. package/dist/tui/components/ChatMessage.js +23 -0
  33. package/dist/tui/components/InputArea.js +23 -0
  34. package/dist/tui/components/Separator.js +7 -0
  35. package/dist/tui/components/StatusBar.js +25 -0
  36. package/dist/tui/components/SteerQueue.js +7 -0
  37. package/dist/tui/components/StreamingText.js +5 -0
  38. package/dist/tui/components/ThinkingIndicator.js +20 -0
  39. package/dist/tui/components/ToolCall.js +11 -0
  40. package/dist/tui/components/UserMessage.js +5 -0
  41. package/dist/tui/hooks/useKeyboardShortcuts.js +89 -0
  42. package/dist/tui/hooks/useSlashCommands.js +52 -0
  43. package/dist/tui/index.js +5 -0
  44. package/dist/tui/ink-entry.js +271 -0
  45. package/dist/tui/menu-mode.js +86 -0
  46. package/dist/tui/tool-render.js +43 -0
  47. package/dist/tui.js +378 -252
  48. 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,5 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Text } from "ink";
3
+ export function StreamingText({ text }) {
4
+ return _jsx(Text, { children: text });
5
+ }
@@ -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";