@phren/agent 0.1.6 → 0.1.8
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/commands/info.js +1 -0
- package/dist/index.js +1 -0
- package/dist/tui/components/App.js +4 -4
- package/dist/tui/components/CodeBlock.js +8 -0
- package/dist/tui/components/ToolCall.js +10 -4
- package/dist/tui/hooks/useKeyboardShortcuts.js +22 -1
- package/dist/tui/ink-entry.js +15 -14
- package/package.json +2 -2
package/dist/commands/info.js
CHANGED
|
@@ -25,6 +25,7 @@ export function helpCommand(_parts, _ctx) {
|
|
|
25
25
|
/compact Compact conversation to save context space
|
|
26
26
|
/context Show context window usage and provider info
|
|
27
27
|
/mode Toggle input mode (steering ↔ queue)
|
|
28
|
+
/verbose Toggle verbose tool output (show first 5 lines)
|
|
28
29
|
/spawn <name> <task> Spawn a background agent
|
|
29
30
|
/agents List running agents
|
|
30
31
|
/session Show session info (id, duration, stats)
|
package/dist/index.js
CHANGED
|
@@ -268,6 +268,7 @@ export async function runAgentCli(raw) {
|
|
|
268
268
|
const costStr = result.totalCost ? `, ${result.totalCost}` : "";
|
|
269
269
|
process.stderr.write(`\nDone: ${result.turns} turns, ${result.toolCalls} tool calls${costStr}\n`);
|
|
270
270
|
}
|
|
271
|
+
process.stdout.write("\x07"); // bell on completion
|
|
271
272
|
// End session with summary + memory intelligence
|
|
272
273
|
if (phrenCtx && sessionId) {
|
|
273
274
|
const summary = result.finalText.slice(0, 500);
|
|
@@ -7,7 +7,7 @@ import { ThinkingIndicator } from "./ThinkingIndicator.js";
|
|
|
7
7
|
import { SteerQueue } from "./SteerQueue.js";
|
|
8
8
|
import { InputArea, PermissionsLine } from "./InputArea.js";
|
|
9
9
|
import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts.js";
|
|
10
|
-
export function App({ state, completedMessages, streamingText, completedToolCalls, thinking, thinkStartTime, thinkElapsed, steerQueue, running, showBanner, inputHistory, onSubmit, onPermissionCycle, onCancelTurn, onExit, }) {
|
|
10
|
+
export function App({ state, completedMessages, streamingText, completedToolCalls, thinking, thinkStartTime, thinkElapsed, steerQueue, running, showBanner, inputHistory, verbose, onSubmit, onPermissionCycle, onCancelTurn, onExit, }) {
|
|
11
11
|
const { exit } = useApp();
|
|
12
12
|
const [inputValue, setInputValue] = useState("");
|
|
13
13
|
const [bashMode, setBashMode] = useState(false);
|
|
@@ -54,15 +54,15 @@ export function App({ state, completedMessages, streamingText, completedToolCall
|
|
|
54
54
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(Banner, { version: state.version }) }, "banner"));
|
|
55
55
|
}
|
|
56
56
|
if (item.kind === "user") {
|
|
57
|
-
return (_jsx(Box, { flexDirection: "column",
|
|
57
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { bold: true, children: ["\u276f", " ", item.text] }) }, item.id));
|
|
58
58
|
}
|
|
59
59
|
if (item.kind === "assistant") {
|
|
60
60
|
const aMsg = item;
|
|
61
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1,
|
|
61
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [aMsg.text ? (_jsxs(Box, { children: [_jsxs(Text, { color: "magenta", children: ["\u25c6", " "] }), _jsx(Text, { children: aMsg.text })] })) : null, aMsg.toolCalls?.map((tc, i) => (_jsx(ToolCall, { ...tc, verbose: verbose }, i)))] }, item.id));
|
|
62
62
|
}
|
|
63
63
|
if (item.kind === "status") {
|
|
64
64
|
return _jsx(Text, { dimColor: true, children: item.text }, item.id);
|
|
65
65
|
}
|
|
66
66
|
return null;
|
|
67
|
-
} }), _jsxs(Box, { flexDirection: "column", children: [completedToolCalls.map((tc, i) => (_jsx(ToolCall, { ...tc }, `tc-${i}`))), streamingText !== "" && (_jsxs(Box, {
|
|
67
|
+
} }), _jsxs(Box, { flexDirection: "column", children: [completedToolCalls.map((tc, i) => (_jsx(ToolCall, { ...tc, verbose: verbose }, `tc-${i}`))), streamingText !== "" && (_jsxs(Box, { children: [_jsxs(Text, { color: "magenta", children: ["\u25c6", " "] }), _jsx(Text, { children: 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: true }), _jsx(PermissionsLine, { mode: state.permMode })] })] }));
|
|
68
68
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { highlightCode } from '../../multi/syntax-highlight.js';
|
|
4
|
+
export function CodeBlock({ code, language }) {
|
|
5
|
+
const lang = language || 'generic';
|
|
6
|
+
const highlighted = highlightCode(code, lang);
|
|
7
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: '```' + lang }), _jsx(Text, { children: highlighted }), _jsx(Text, { dimColor: true, children: '```' })] }));
|
|
8
|
+
}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import { formatToolInput, formatDuration
|
|
4
|
-
|
|
3
|
+
import { formatToolInput, formatDuration } from "../tool-render.js";
|
|
4
|
+
const VERBOSE_LINES = 5;
|
|
5
|
+
export function ToolCall({ name, input, output, isError, durationMs, verbose }) {
|
|
5
6
|
const preview = formatToolInput(name, input);
|
|
6
7
|
const dur = formatDuration(durationMs);
|
|
8
|
+
if (!verbose) {
|
|
9
|
+
// Non-verbose: header only, no output body
|
|
10
|
+
return (_jsx(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] })] }) }));
|
|
11
|
+
}
|
|
12
|
+
// Verbose: header + first 5 lines of output + overflow count
|
|
7
13
|
const allLines = output.split("\n").filter(Boolean);
|
|
8
|
-
const shown = allLines.slice(0,
|
|
9
|
-
const overflow = allLines.length -
|
|
14
|
+
const shown = allLines.slice(0, VERBOSE_LINES);
|
|
15
|
+
const overflow = allLines.length - VERBOSE_LINES;
|
|
10
16
|
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
17
|
}
|
|
@@ -3,7 +3,7 @@ const SLASH_COMMANDS = [
|
|
|
3
3
|
"/help", "/turns", "/clear", "/cwd", "/files", "/cost", "/plan", "/undo",
|
|
4
4
|
"/context", "/model", "/provider", "/preset", "/session", "/history",
|
|
5
5
|
"/compact", "/diff", "/git", "/mem", "/ask", "/spawn", "/agents",
|
|
6
|
-
"/mode", "/exit", "/quit",
|
|
6
|
+
"/mode", "/verbose", "/exit", "/quit",
|
|
7
7
|
];
|
|
8
8
|
export function useKeyboardShortcuts(opts) {
|
|
9
9
|
useInput((input, key) => {
|
|
@@ -17,6 +17,11 @@ export function useKeyboardShortcuts(opts) {
|
|
|
17
17
|
opts.onExit();
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
+
// Ctrl+L -- clear screen
|
|
21
|
+
if (key.ctrl && input === "l") {
|
|
22
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
20
25
|
// Shift+Tab -- cycle permission mode
|
|
21
26
|
if (key.shift && key.tab) {
|
|
22
27
|
opts.onCyclePermissions();
|
|
@@ -74,6 +79,22 @@ export function useKeyboardShortcuts(opts) {
|
|
|
74
79
|
opts.onSetInput(history[history.length - 1 - newIndex]);
|
|
75
80
|
return;
|
|
76
81
|
}
|
|
82
|
+
// Ctrl+W or Ctrl+Backspace -- delete last word
|
|
83
|
+
if ((key.ctrl && input === "w") || (key.ctrl && key.backspace) || input === "\x1b\x7f") {
|
|
84
|
+
const val = opts.inputValue;
|
|
85
|
+
const trimmed = val.replace(/\s+$/, ""); // strip trailing spaces
|
|
86
|
+
const lastSpace = trimmed.lastIndexOf(" ");
|
|
87
|
+
opts.onSetInput(lastSpace >= 0 ? trimmed.slice(0, lastSpace + 1) : "");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// Ctrl+U -- clear entire line
|
|
91
|
+
if (key.ctrl && input === "u") {
|
|
92
|
+
opts.onSetInput("");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// Ctrl+A -- move cursor to start (clear and re-set — Ink TextInput doesn't expose cursor)
|
|
96
|
+
// Ctrl+E -- move cursor to end (no-op, cursor is always at end in Ink TextInput)
|
|
97
|
+
// These are limited by ink-text-input which doesn't support cursor positioning
|
|
77
98
|
// Tab -- slash command completion when input starts with /
|
|
78
99
|
if (key.tab && !key.shift && !opts.isRunning) {
|
|
79
100
|
const val = opts.inputValue;
|
package/dist/tui/ink-entry.js
CHANGED
|
@@ -21,6 +21,7 @@ export async function startInkTui(config, spawner) {
|
|
|
21
21
|
const steerQueueBuf = [];
|
|
22
22
|
const inputHistory = [];
|
|
23
23
|
let running = false;
|
|
24
|
+
let verbose = false;
|
|
24
25
|
let msgCounter = 0;
|
|
25
26
|
// Mutable render state — updated then pushed to React via rerender()
|
|
26
27
|
const completedMessages = [];
|
|
@@ -48,7 +49,7 @@ export async function startInkTui(config, spawner) {
|
|
|
48
49
|
function update() {
|
|
49
50
|
if (!rerender)
|
|
50
51
|
return;
|
|
51
|
-
rerender(_jsx(App, { state: getAppState(), completedMessages: [...completedMessages], streamingText: streamingText, completedToolCalls: [...currentToolCalls], thinking: thinking, thinkStartTime: thinkStartTime, thinkElapsed: thinkElapsed, steerQueue: [...steerQueueBuf], running: running, showBanner: true, inputHistory: [...inputHistory], onSubmit: handleSubmit, onPermissionCycle: handlePermissionCycle, onCancelTurn: handleCancelTurn, onExit: handleExit }));
|
|
52
|
+
rerender(_jsx(App, { state: getAppState(), completedMessages: [...completedMessages], streamingText: streamingText, completedToolCalls: [...currentToolCalls], thinking: thinking, thinkStartTime: thinkStartTime, thinkElapsed: thinkElapsed, steerQueue: [...steerQueueBuf], running: running, showBanner: true, inputHistory: [...inputHistory], verbose: verbose, onSubmit: handleSubmit, onPermissionCycle: handlePermissionCycle, onCancelTurn: handleCancelTurn, onExit: handleExit }));
|
|
52
53
|
}
|
|
53
54
|
function handlePermissionCycle() {
|
|
54
55
|
const next = nextPermissionMode(config.registry.permissionConfig.mode);
|
|
@@ -99,7 +100,6 @@ export async function startInkTui(config, spawner) {
|
|
|
99
100
|
},
|
|
100
101
|
});
|
|
101
102
|
function handleSubmit(input) {
|
|
102
|
-
console.error(`[BRIDGE] handleSubmit: ${JSON.stringify(input.slice(0, 40))} running=${running}`);
|
|
103
103
|
const line = input.trim();
|
|
104
104
|
if (!line)
|
|
105
105
|
return;
|
|
@@ -147,6 +147,12 @@ export async function startInkTui(config, spawner) {
|
|
|
147
147
|
update();
|
|
148
148
|
return;
|
|
149
149
|
}
|
|
150
|
+
if (line === "/verbose") {
|
|
151
|
+
verbose = !verbose;
|
|
152
|
+
completedMessages.push({ id: nextId(), kind: "status", text: `Verbose: ${verbose ? "on" : "off"}` });
|
|
153
|
+
update();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
150
156
|
// Slash commands — capture stderr output and display as status message
|
|
151
157
|
if (line.startsWith("/")) {
|
|
152
158
|
if (slashCommands.tryHandleCommand(line)) {
|
|
@@ -173,34 +179,28 @@ export async function startInkTui(config, spawner) {
|
|
|
173
179
|
// TurnHooks bridge — updates mutable state, calls update()
|
|
174
180
|
const tuiHooks = {
|
|
175
181
|
onTextDelta: (text) => {
|
|
176
|
-
console.error(`[BRIDGE] onTextDelta: ${JSON.stringify(text.slice(0, 40))}`);
|
|
177
182
|
thinking = false;
|
|
178
183
|
streamingText += text;
|
|
179
184
|
update();
|
|
180
185
|
},
|
|
181
186
|
onTextDone: () => {
|
|
182
|
-
console.error("[BRIDGE] onTextDone");
|
|
183
187
|
// streaming complete — finalized in runAgentTurn
|
|
184
188
|
},
|
|
185
189
|
onTextBlock: (text) => {
|
|
186
|
-
console.error(`[BRIDGE] onTextBlock: ${JSON.stringify(text.slice(0, 40))}`);
|
|
187
190
|
thinking = false;
|
|
188
191
|
streamingText += text;
|
|
189
192
|
update();
|
|
190
193
|
},
|
|
191
194
|
onToolStart: (_name, _input, _count) => {
|
|
192
|
-
console.error(`[BRIDGE] onToolStart: ${_name} (${_count} tools)`);
|
|
193
195
|
thinking = false;
|
|
194
196
|
update();
|
|
195
197
|
},
|
|
196
198
|
onToolEnd: (name, input, output, isError, dur) => {
|
|
197
|
-
console.error(`[BRIDGE] onToolEnd: ${name} isError=${isError} dur=${dur}ms`);
|
|
198
199
|
const diffData = (name === "edit_file" || name === "write_file") ? decodeDiffPayload(output) : null;
|
|
199
200
|
const cleanOutput = diffData ? output.slice(0, output.indexOf(DIFF_MARKER)) : output;
|
|
200
201
|
currentToolCalls.push({ name, input, output: cleanOutput, isError, durationMs: dur });
|
|
201
202
|
update();
|
|
202
203
|
},
|
|
203
|
-
onStatus: (msg) => { console.error(`[BRIDGE] onStatus: ${msg.slice(0, 60)}`); update(); },
|
|
204
204
|
getSteeringInput: () => {
|
|
205
205
|
const result = (() => {
|
|
206
206
|
if (steerQueueBuf.length > 0 && inputMode === "steering") {
|
|
@@ -213,13 +213,10 @@ export async function startInkTui(config, spawner) {
|
|
|
213
213
|
}
|
|
214
214
|
return null;
|
|
215
215
|
})();
|
|
216
|
-
if (result)
|
|
217
|
-
console.error(`[BRIDGE] getSteeringInput: ${JSON.stringify(result.slice(0, 40))}`);
|
|
218
216
|
return result;
|
|
219
217
|
},
|
|
220
218
|
};
|
|
221
219
|
async function runAgentTurn(userInput) {
|
|
222
|
-
console.error(`[BRIDGE] runAgentTurn START: ${JSON.stringify(userInput.slice(0, 40))}`);
|
|
223
220
|
running = true;
|
|
224
221
|
thinking = true;
|
|
225
222
|
thinkStartTime = Date.now();
|
|
@@ -232,12 +229,10 @@ export async function startInkTui(config, spawner) {
|
|
|
232
229
|
}
|
|
233
230
|
catch (err) {
|
|
234
231
|
const msg = err instanceof Error ? err.message : String(err);
|
|
235
|
-
console.error(`[BRIDGE] runAgentTurn ERROR: ${msg}`);
|
|
236
232
|
streamingText += `\nError: ${msg}`;
|
|
237
233
|
}
|
|
238
234
|
// Compute elapsed time
|
|
239
235
|
const elapsed = ((Date.now() - thinkStartTime) / 1000).toFixed(1);
|
|
240
|
-
console.error(`[BRIDGE] runAgentTurn END: elapsed=${elapsed}s streamingText=${streamingText.length}chars toolCalls=${currentToolCalls.length}`);
|
|
241
236
|
// Finalize: move streaming content + tool calls to completed messages
|
|
242
237
|
thinking = false;
|
|
243
238
|
if (streamingText || currentToolCalls.length > 0) {
|
|
@@ -273,13 +268,19 @@ export async function startInkTui(config, spawner) {
|
|
|
273
268
|
runAgentTurn(queued);
|
|
274
269
|
}
|
|
275
270
|
}
|
|
271
|
+
// Enable bracketed paste
|
|
272
|
+
process.stdout.write("\x1b[?2004h");
|
|
276
273
|
// Clear screen before initial render — start clean
|
|
277
274
|
process.stdout.write("\x1b[2J\x1b[H");
|
|
275
|
+
const projectName = config.phrenCtx?.project ?? "phren";
|
|
276
|
+
process.title = "phren";
|
|
277
|
+
process.stdout.write(`\x1b]0;phren \xb7 ${projectName}\x07`); // set terminal window title
|
|
278
278
|
// Initial render
|
|
279
|
-
const app = render(_jsx(App, { state: getAppState(), completedMessages: [], streamingText: "", completedToolCalls: [], thinking: false, thinkStartTime: 0, thinkElapsed: null, steerQueue: [], running: false, showBanner: true, inputHistory: [], onSubmit: handleSubmit, onPermissionCycle: handlePermissionCycle, onCancelTurn: handleCancelTurn, onExit: handleExit }), { exitOnCtrlC: false });
|
|
279
|
+
const app = render(_jsx(App, { state: getAppState(), completedMessages: [], streamingText: "", completedToolCalls: [], thinking: false, thinkStartTime: 0, thinkElapsed: null, steerQueue: [], running: false, showBanner: true, inputHistory: [], verbose: verbose, onSubmit: handleSubmit, onPermissionCycle: handlePermissionCycle, onCancelTurn: handleCancelTurn, onExit: handleExit }), { exitOnCtrlC: false });
|
|
280
280
|
rerender = app.rerender;
|
|
281
281
|
const done = new Promise((r) => { resolveSession = r; });
|
|
282
282
|
app.waitUntilExit().then(() => {
|
|
283
|
+
process.stdout.write("\x1b[?2004l"); // disable bracketed paste
|
|
283
284
|
if (resolveSession)
|
|
284
285
|
resolveSession(session);
|
|
285
286
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phren/agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Coding agent with persistent memory — powered by phren",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"ink-spinner": "^5.0.0",
|
|
18
18
|
"ink-text-input": "^6.0.0",
|
|
19
19
|
"react": "^19.2.4",
|
|
20
|
-
"@phren/cli": "0.1.
|
|
20
|
+
"@phren/cli": "0.1.8"
|
|
21
21
|
},
|
|
22
22
|
"engines": {
|
|
23
23
|
"node": ">=20.0.0"
|