@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.
@@ -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", marginTop: 1, children: _jsxs(Text, { bold: true, children: ["\u276f", " ", item.text] }) }, item.id));
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, paddingLeft: 2, 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 }, i)))] }, item.id));
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, { paddingLeft: 2, 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: !running }), _jsx(PermissionsLine, { mode: state.permMode })] })] }));
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, COMPACT_LINES } from "../tool-render.js";
4
- export function ToolCall({ name, input, output, isError, durationMs }) {
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, COMPACT_LINES);
9
- const overflow = allLines.length - COMPACT_LINES;
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;
@@ -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.6",
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.6"
20
+ "@phren/cli": "0.1.8"
21
21
  },
22
22
  "engines": {
23
23
  "node": ">=20.0.0"