@devang0907/agent-dev 0.1.3 → 0.1.5

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/ui/App.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useEffect, useCallback, useRef } from "react";
3
- import { Box, useInput, useApp } from "ink";
3
+ import { Box, useInput, useApp, useStdout } from "ink";
4
4
  import { ChatView } from "./ChatView.js";
5
5
  import { Editor } from "./Editor.js";
6
6
  import { Footer } from "./Footer.js";
@@ -9,8 +9,15 @@ import { ApiKeyPrompt } from "./ApiKeyPrompt.js";
9
9
  import { SettingsView } from "./SettingsView.js";
10
10
  import { hasProviderAuth, getDefaultModelForProvider } from "../providers/registry.js";
11
11
  import { findModel } from "../config/models.js";
12
+ import { CommandApprovalPrompt } from "./CommandApprovalPrompt.js";
12
13
  import { StartupBanner } from "./StartupBanner.js";
13
14
  import { getTheme } from "./theme.js";
15
+ import { scrollViewportToBottom } from "./scroll.js";
16
+ import { formatToolForDisplay } from "./format-tool.js";
17
+ let nextMessageId = 0;
18
+ function toDisplayMessage(role, content, toolName) {
19
+ return { id: nextMessageId++, role, content, toolName };
20
+ }
14
21
  function modelForProvider(provider, settings) {
15
22
  const current = findModel(settings.defaultProvider, settings.defaultModel);
16
23
  if (current?.provider === provider)
@@ -19,11 +26,7 @@ function modelForProvider(provider, settings) {
19
26
  }
20
27
  export function App({ session, workdir, onQuit }) {
21
28
  const { exit } = useApp();
22
- const [displayMessages, setDisplayMessages] = useState(() => session.getMessages().map((m) => ({
23
- role: m.role === "tool" ? "tool" : m.role === "user" ? "user" : "assistant",
24
- content: m.content,
25
- toolName: m.name,
26
- })));
29
+ const [displayMessages, setDisplayMessages] = useState(() => session.getMessages().map((m) => toDisplayMessage(m.role === "tool" ? "tool" : m.role === "user" ? "user" : "assistant", m.content, m.name)));
27
30
  const [streamingText, setStreamingText] = useState("");
28
31
  const [overlay, setOverlay] = useState("none");
29
32
  const [modelFilter, setModelFilter] = useState();
@@ -32,9 +35,13 @@ export function App({ session, workdir, onQuit }) {
32
35
  const [settings, setSettings] = useState(session.getSettings());
33
36
  const [model, setModel] = useState(session.getModel());
34
37
  const [running, setRunning] = useState(false);
38
+ const [autoFollow, setAutoFollow] = useState(true);
39
+ const [pendingCommand, setPendingCommand] = useState(null);
35
40
  const streamingRef = useRef("");
41
+ const autoFollowRef = useRef(true);
36
42
  const startupChecked = useRef(false);
37
43
  const theme = getTheme();
44
+ const { stdout } = useStdout();
38
45
  const openApiKeyPrompt = useCallback((target, returnTo = "none") => {
39
46
  setPendingModel(target);
40
47
  setApiKeyReturnOverlay(returnTo);
@@ -65,18 +72,28 @@ export function App({ session, workdir, onQuit }) {
65
72
  openApiKeyPrompt(current, "none");
66
73
  }
67
74
  }, [session, settings, openApiKeyPrompt]);
75
+ useEffect(() => {
76
+ if (autoFollow && streamingText) {
77
+ scrollViewportToBottom(stdout);
78
+ }
79
+ }, [streamingText, autoFollow, stdout]);
68
80
  useEffect(() => {
69
81
  const handler = (event) => {
70
82
  switch (event.type) {
71
83
  case "user_message":
72
- setDisplayMessages((prev) => [...prev, { role: "user", content: event.content }]);
84
+ autoFollowRef.current = true;
85
+ setAutoFollow(true);
86
+ setDisplayMessages((prev) => [...prev, toDisplayMessage("user", event.content)]);
73
87
  setRunning(true);
74
88
  streamingRef.current = "";
75
89
  setStreamingText("");
90
+ scrollViewportToBottom(stdout);
76
91
  break;
77
92
  case "message_start":
78
93
  streamingRef.current = "";
79
94
  setStreamingText("");
95
+ if (autoFollowRef.current)
96
+ scrollViewportToBottom(stdout);
80
97
  break;
81
98
  case "text_delta":
82
99
  streamingRef.current += event.delta;
@@ -85,7 +102,7 @@ export function App({ session, workdir, onQuit }) {
85
102
  case "tool_call":
86
103
  const partial = streamingRef.current;
87
104
  if (partial) {
88
- setDisplayMessages((prev) => [...prev, { role: "assistant", content: partial }]);
105
+ setDisplayMessages((prev) => [...prev, toDisplayMessage("assistant", partial)]);
89
106
  streamingRef.current = "";
90
107
  setStreamingText("");
91
108
  }
@@ -93,13 +110,19 @@ export function App({ session, workdir, onQuit }) {
93
110
  case "tool_result":
94
111
  setDisplayMessages((prev) => [
95
112
  ...prev,
96
- { role: "tool", content: event.result, toolName: event.name },
113
+ toDisplayMessage("tool", formatToolForDisplay(event.name, event.result), event.name),
97
114
  ]);
98
115
  break;
99
116
  case "turn_end":
100
117
  const final = streamingRef.current;
101
118
  if (final) {
102
- setDisplayMessages((prev) => [...prev, { role: "assistant", content: final }]);
119
+ setDisplayMessages((prev) => {
120
+ const last = prev[prev.length - 1];
121
+ if (last?.role === "assistant" && last.content.trim() === final.trim()) {
122
+ return prev;
123
+ }
124
+ return [...prev, toDisplayMessage("assistant", final)];
125
+ });
103
126
  }
104
127
  streamingRef.current = "";
105
128
  setStreamingText("");
@@ -108,15 +131,20 @@ export function App({ session, workdir, onQuit }) {
108
131
  case "error":
109
132
  setDisplayMessages((prev) => [
110
133
  ...prev,
111
- { role: "assistant", content: `Error: ${event.message}` },
134
+ toDisplayMessage("assistant", `Error: ${event.message}`),
112
135
  ]);
113
136
  streamingRef.current = "";
114
137
  setStreamingText("");
115
138
  setRunning(false);
139
+ setPendingCommand(null);
116
140
  if (/Missing .*API_KEY/i.test(event.message)) {
117
141
  openApiKeyPrompt(model, "none");
118
142
  }
119
143
  break;
144
+ case "permission_request":
145
+ setPendingCommand(event.request);
146
+ setOverlay("commandApproval");
147
+ break;
120
148
  case "model_changed":
121
149
  setModel(event.model);
122
150
  break;
@@ -126,12 +154,24 @@ export function App({ session, workdir, onQuit }) {
126
154
  return () => {
127
155
  session.off("event", handler);
128
156
  };
129
- }, [session, model, openApiKeyPrompt]);
130
- useInput((_, key) => {
157
+ }, [session, model, openApiKeyPrompt, stdout]);
158
+ autoFollowRef.current = autoFollow;
159
+ useInput((input, key) => {
131
160
  if (overlay !== "none")
132
161
  return;
133
162
  if (key.escape && running) {
134
163
  session.abort();
164
+ return;
165
+ }
166
+ if (key.pageUp || (key.upArrow && key.shift)) {
167
+ autoFollowRef.current = false;
168
+ setAutoFollow(false);
169
+ return;
170
+ }
171
+ if (input === "g" && key.ctrl) {
172
+ autoFollowRef.current = true;
173
+ setAutoFollow(true);
174
+ scrollViewportToBottom(stdout);
135
175
  }
136
176
  }, { isActive: overlay === "none" });
137
177
  const handleSubmit = useCallback(async (value) => {
@@ -144,6 +184,7 @@ export function App({ session, workdir, onQuit }) {
144
184
  session.newSession();
145
185
  setDisplayMessages([]);
146
186
  setStreamingText("");
187
+ setAutoFollow(true);
147
188
  return;
148
189
  }
149
190
  if (value === "/settings") {
@@ -165,7 +206,10 @@ export function App({ session, workdir, onQuit }) {
165
206
  await session.prompt(value);
166
207
  }, [session, running, onQuit, exit, model, settings, openApiKeyPrompt]);
167
208
  const hasChat = displayMessages.length > 0 || streamingText.length > 0;
168
- return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsx(Box, { paddingX: 2, marginBottom: 1, children: _jsx(StartupBanner, { theme: theme, compact: hasChat }) }), _jsx(ChatView, { messages: displayMessages, theme: theme, model: model, streamingText: streamingText, running: running }), _jsx(Footer, { workdir: workdir, model: model, theme: theme }), overlay === "none" && (_jsx(Editor, { theme: theme, model: model, disabled: running, running: running, onSubmit: handleSubmit })), overlay === "model" && (_jsx(ModelSelector, { theme: theme, settings: settings, filter: modelFilter, onSelect: (m) => {
209
+ return (_jsxs(Box, { flexDirection: "column", children: [!hasChat && (_jsx(Box, { paddingX: 2, marginBottom: 1, flexShrink: 0, children: _jsx(StartupBanner, { theme: theme }) })), _jsx(ChatView, { messages: displayMessages, theme: theme, model: model, streamingText: streamingText, running: running, autoFollow: autoFollow }), _jsx(Footer, { workdir: workdir, model: model, theme: theme }), overlay === "none" && (_jsx(Box, { flexShrink: 0, children: _jsx(Editor, { theme: theme, model: model, disabled: running, running: running, onSubmit: handleSubmit, onPauseFollow: () => {
210
+ autoFollowRef.current = false;
211
+ setAutoFollow(false);
212
+ } }) })), overlay === "model" && (_jsx(ModelSelector, { theme: theme, settings: settings, filter: modelFilter, onSelect: (m) => {
169
213
  if (!hasProviderAuth(m.provider, settings)) {
170
214
  openApiKeyPrompt(m, "model");
171
215
  return;
@@ -186,5 +230,13 @@ export function App({ session, workdir, onQuit }) {
186
230
  setSettings(s);
187
231
  }, onSetApiKey: (provider) => {
188
232
  openApiKeyPrompt(modelForProvider(provider, settings), "settings");
189
- }, onClose: () => setOverlay("none") }))] }));
233
+ }, onClose: () => setOverlay("none") })), overlay === "commandApproval" && pendingCommand && (_jsx(CommandApprovalPrompt, { theme: theme, request: pendingCommand, onApprove: () => {
234
+ session.respondToPermission(true);
235
+ setPendingCommand(null);
236
+ setOverlay("none");
237
+ }, onDeny: () => {
238
+ session.respondToPermission(false);
239
+ setPendingCommand(null);
240
+ setOverlay("none");
241
+ } }))] }));
190
242
  }
@@ -8,6 +8,7 @@ interface ChatViewProps {
8
8
  model: Model;
9
9
  streamingText?: string;
10
10
  running?: boolean;
11
+ autoFollow?: boolean;
11
12
  }
12
- export declare function ChatView({ messages, theme, model, streamingText, running }: ChatViewProps): React.JSX.Element | null;
13
+ export declare function ChatView({ messages, theme, model, streamingText, running, autoFollow, }: ChatViewProps): React.JSX.Element | null;
13
14
  export {};
@@ -1,14 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useEffect } from "react";
3
- import { Box, Text } from "ink";
2
+ import { useEffect, useState } from "react";
3
+ import { Box, Static, Text } from "ink";
4
4
  import { SPINNER_FRAMES, TOOL_ICONS } from "./theme.js";
5
5
  import { LeftBorder } from "./LeftBorder.js";
6
- import { Panel } from "./Panel.js";
7
6
  import { modelRef } from "../config/models.js";
8
- function truncate(text, max) {
9
- return text.length > max ? text.slice(0, max) + "" : text;
7
+ function StaticMessage({ msg, theme, model, showModelTag, }) {
8
+ if (msg.role === "user") {
9
+ return (_jsx(Box, { marginBottom: 1, children: _jsx(LeftBorder, { theme: theme, borderColor: theme.primary, marginBottom: 0, children: _jsx(Text, { color: theme.text, children: msg.content }) }) }));
10
+ }
11
+ if (msg.role === "assistant") {
12
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, paddingLeft: 1, children: [_jsx(Text, { color: theme.text, children: msg.content || "" }), showModelTag && (_jsxs(Text, { color: theme.textMuted, children: [_jsx(Text, { color: theme.primary, children: "\u25A3 " }), modelRef(model)] }))] }));
13
+ }
14
+ const icon = TOOL_ICONS[msg.toolName ?? ""] ?? "·";
15
+ return (_jsx(Box, { paddingLeft: 1, marginBottom: 0, children: _jsxs(Text, { color: theme.textMuted, children: [icon, " ", msg.content] }) }));
10
16
  }
11
- export function ChatView({ messages, theme, model, streamingText, running }) {
17
+ export function ChatView({ messages, theme, model, streamingText, running, autoFollow = true, }) {
12
18
  const [spinIdx, setSpinIdx] = useState(0);
13
19
  const hasContent = messages.length > 0 || (streamingText?.length ?? 0) > 0;
14
20
  useEffect(() => {
@@ -20,5 +26,7 @@ export function ChatView({ messages, theme, model, streamingText, running }) {
20
26
  if (!hasContent) {
21
27
  return null;
22
28
  }
23
- return (_jsxs(Panel, { theme: theme, flexGrow: 1, borderColor: theme.border, children: [messages.map((msg, i) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [msg.role === "user" && (_jsx(LeftBorder, { theme: theme, borderColor: theme.primary, marginBottom: 0, children: _jsx(Text, { color: theme.text, children: msg.content }) })), msg.role === "assistant" && (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [_jsx(Text, { color: theme.text, children: msg.content || "" }), _jsxs(Text, { color: theme.textMuted, children: [_jsx(Text, { color: theme.primary, children: "\u25A3 " }), modelRef(model)] })] })), msg.role === "tool" && (_jsx(Box, { paddingLeft: 1, children: _jsxs(Text, { color: theme.text, children: [_jsx(Text, { color: theme.textMuted, children: TOOL_ICONS[msg.toolName ?? ""] ?? "⚙" }), " ", _jsx(Text, { bold: true, children: msg.toolName }), _jsxs(Text, { color: theme.textMuted, children: [" ", truncate(msg.content, 200)] })] }) }))] }, i))), streamingText && (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [_jsx(Text, { color: theme.text, children: streamingText }), _jsxs(Text, { color: theme.textMuted, children: [_jsxs(Text, { color: theme.primary, children: [SPINNER_FRAMES[spinIdx], " "] }), "responding\u2026"] })] })), running && !streamingText && messages.length > 0 && (_jsx(Box, { paddingLeft: 1, children: _jsxs(Text, { color: theme.textMuted, children: [_jsx(Text, { color: theme.primary, children: SPINNER_FRAMES[spinIdx] }), " working\u2026"] }) }))] }));
29
+ const lastAssistantId = [...messages].reverse().find((m) => m.role === "assistant")?.id;
30
+ const showWorking = running && !streamingText && messages.length > 0;
31
+ return (_jsxs(Box, { flexDirection: "column", marginX: 2, marginBottom: 1, children: [_jsx(Static, { items: messages, children: (msg) => (_jsx(StaticMessage, { msg: msg, theme: theme, model: model, showModelTag: msg.role === "assistant" && msg.id === lastAssistantId && !running }, msg.id)) }), streamingText && (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, marginTop: 1, children: [_jsx(Text, { color: theme.text, children: streamingText }), _jsxs(Text, { color: theme.textMuted, children: [_jsxs(Text, { color: theme.primary, children: [SPINNER_FRAMES[spinIdx], " "] }), !autoFollow && _jsx(Text, { color: theme.warning, children: "follow paused \u00B7 Ctrl+G " })] })] })), showWorking && (_jsx(Box, { paddingLeft: 1, marginTop: 1, children: _jsxs(Text, { color: theme.textMuted, children: [_jsx(Text, { color: theme.primary, children: SPINNER_FRAMES[spinIdx] }), " working\u2026"] }) }))] }));
24
32
  }
@@ -0,0 +1,11 @@
1
+ import React from "react";
2
+ import type { ThemeColors } from "./theme.js";
3
+ import type { PermissionRequest } from "../agent/loop.js";
4
+ interface CommandApprovalPromptProps {
5
+ theme: ThemeColors;
6
+ request: PermissionRequest;
7
+ onApprove: () => void;
8
+ onDeny: () => void;
9
+ }
10
+ export declare function CommandApprovalPrompt({ theme, request, onApprove, onDeny, }: CommandApprovalPromptProps): React.JSX.Element;
11
+ export {};
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { LeftBorder } from "./LeftBorder.js";
4
+ export function CommandApprovalPrompt({ theme, request, onApprove, onDeny, }) {
5
+ useInput((input, key) => {
6
+ if (input === "y" || input === "Y") {
7
+ onApprove();
8
+ return;
9
+ }
10
+ if (input === "n" || input === "N" || key.escape) {
11
+ onDeny();
12
+ }
13
+ }, { isActive: true });
14
+ return (_jsx(Box, { flexDirection: "column", marginX: 2, marginTop: 1, marginBottom: 1, children: _jsxs(LeftBorder, { theme: theme, borderColor: theme.warning, children: [_jsx(Text, { color: theme.text, bold: true, children: "Run shell command?" }), _jsx(Text, { color: theme.textMuted, children: " y approve \u00B7 n or Esc deny" }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: theme.warning, paddingX: 1, paddingY: 0, children: _jsx(Text, { color: theme.text, children: request.command }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.textMuted, children: "The agent wants to run this command in your project directory." }) })] }) }));
15
+ }
@@ -7,6 +7,7 @@ interface EditorProps {
7
7
  disabled?: boolean;
8
8
  running?: boolean;
9
9
  onSubmit: (value: string) => void;
10
+ onPauseFollow?: () => void;
10
11
  }
11
- export declare function Editor({ theme, model, disabled, running, onSubmit }: EditorProps): React.JSX.Element;
12
+ export declare function Editor({ theme, model, disabled, running, onSubmit, onPauseFollow }: EditorProps): React.JSX.Element;
12
13
  export {};
package/dist/ui/Editor.js CHANGED
@@ -10,7 +10,7 @@ function BlinkingCursor({ theme, visible }) {
10
10
  return null;
11
11
  return _jsx(Text, { color: theme.primary, children: "\u258C" });
12
12
  }
13
- export function Editor({ theme, model, disabled, running, onSubmit }) {
13
+ export function Editor({ theme, model, disabled, running, onSubmit, onPauseFollow }) {
14
14
  const [value, setValue] = useState("");
15
15
  const [suggestions, setSuggestions] = useState([]);
16
16
  const [spinIdx, setSpinIdx] = useState(0);
@@ -60,6 +60,10 @@ export function Editor({ theme, model, disabled, running, onSubmit }) {
60
60
  }
61
61
  return;
62
62
  }
63
+ if ((key.pageUp || key.upArrow) && !key.ctrl && !key.meta) {
64
+ onPauseFollow?.();
65
+ return;
66
+ }
63
67
  if (key.backspace || key.delete) {
64
68
  const newVal = value.slice(0, -1);
65
69
  setValue(newVal);
@@ -74,5 +78,5 @@ export function Editor({ theme, model, disabled, running, onSubmit }) {
74
78
  }, { isActive: !disabled });
75
79
  const placeholder = "Ask anything…";
76
80
  const showCursor = !disabled && cursorOn;
77
- return (_jsxs(Box, { flexDirection: "column", marginX: 2, children: [suggestions.length > 0 && (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.border, paddingX: 1, marginBottom: 1, children: suggestions.map((s) => (_jsxs(Text, { children: [_jsx(Text, { color: theme.primary, children: s.cmd }), _jsxs(Text, { color: theme.textMuted, children: [" \u2014 ", s.desc] })] }, s.cmd))) })), _jsxs(Panel, { theme: theme, borderColor: disabled ? theme.border : theme.primary, marginBottom: 0, children: [_jsx(Box, { flexDirection: "row", children: value.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.text, children: value }), _jsx(BlinkingCursor, { theme: theme, visible: showCursor })] })) : showCursor ? (_jsx(BlinkingCursor, { theme: theme, visible: true })) : (_jsx(Text, { color: theme.textMuted, children: placeholder })) }), _jsxs(Text, { color: theme.textMuted, children: ["agent-dev \u00B7 ", _jsx(Text, { color: theme.text, children: modelRef(model) })] })] }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: running ? (_jsxs(Text, { color: theme.textMuted, children: [_jsx(Text, { color: theme.primary, children: SPINNER_FRAMES[spinIdx] }), " ", "esc interrupt"] })) : (_jsx(Text, { color: theme.textMuted, children: "Tab completes /commands" })) })] }));
81
+ return (_jsxs(Box, { flexDirection: "column", marginX: 2, children: [suggestions.length > 0 && (_jsx(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.border, paddingX: 1, marginBottom: 1, children: suggestions.map((s) => (_jsxs(Text, { children: [_jsx(Text, { color: theme.primary, children: s.cmd }), _jsxs(Text, { color: theme.textMuted, children: [" \u2014 ", s.desc] })] }, s.cmd))) })), _jsxs(Panel, { theme: theme, borderColor: disabled ? theme.border : theme.primary, marginBottom: 0, children: [_jsx(Box, { flexDirection: "row", children: value.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.text, children: value }), _jsx(BlinkingCursor, { theme: theme, visible: showCursor })] })) : showCursor ? (_jsx(BlinkingCursor, { theme: theme, visible: true })) : (_jsx(Text, { color: theme.textMuted, children: placeholder })) }), _jsxs(Text, { color: theme.textMuted, children: ["agent-dev \u00B7 ", _jsx(Text, { color: theme.text, children: modelRef(model) })] })] }), _jsx(Box, { marginTop: 1, marginBottom: 1, children: running ? (_jsxs(Text, { color: theme.textMuted, children: [_jsx(Text, { color: theme.primary, children: SPINNER_FRAMES[spinIdx] }), " ", "esc interrupt"] })) : (_jsx(Text, { color: theme.textMuted, children: "Tab completes /commands \u00B7 scroll freely \u00B7 Ctrl+G follow" })) })] }));
78
82
  }
@@ -0,0 +1,2 @@
1
+ /** Compact one-line labels for tool activity in the chat UI. */
2
+ export declare function formatToolForDisplay(toolName: string, result: string): string;
@@ -0,0 +1,25 @@
1
+ /** Compact one-line labels for tool activity in the chat UI. */
2
+ export function formatToolForDisplay(toolName, result) {
3
+ if (toolName === "web_search") {
4
+ const query = result.match(/Headlines for:\s*(.+)/)?.[1]?.trim()
5
+ ?? result.match(/Search results for:\s*(.+)/)?.[1]?.trim();
6
+ return query ? `news: "${query}"` : "searched the web";
7
+ }
8
+ if (toolName === "read") {
9
+ return result.startsWith("Error:") ? result : "read file";
10
+ }
11
+ if (toolName === "write" || toolName === "edit") {
12
+ return result.startsWith("Error:") ? result : result.split("\n")[0] ?? toolName;
13
+ }
14
+ if (toolName === "bash") {
15
+ if (result.includes("Dev server")) {
16
+ const first = result.split("\n").slice(0, 2).join(" · ");
17
+ return first.length > 100 ? first.slice(0, 100) + "…" : first;
18
+ }
19
+ if (result.startsWith("Error:"))
20
+ return result.split("\n")[0] ?? result;
21
+ const line = result.split("\n").find((l) => l.trim()) ?? "command finished";
22
+ return line.length > 80 ? line.slice(0, 80) + "…" : line;
23
+ }
24
+ return result.length > 100 ? result.slice(0, 100) + "…" : result;
25
+ }
@@ -0,0 +1,6 @@
1
+ export interface TerminalSize {
2
+ rows: number;
3
+ cols: number;
4
+ }
5
+ export declare function useTerminalSize(): TerminalSize;
6
+ export declare function chatContentWidth(cols: number): number;
@@ -0,0 +1,22 @@
1
+ import { useStdout } from "ink";
2
+ import { useEffect, useState } from "react";
3
+ export function useTerminalSize() {
4
+ const { stdout } = useStdout();
5
+ const [size, setSize] = useState({
6
+ rows: stdout.rows,
7
+ cols: stdout.columns,
8
+ });
9
+ useEffect(() => {
10
+ const onResize = () => {
11
+ setSize({ rows: stdout.rows, cols: stdout.columns });
12
+ };
13
+ stdout.on("resize", onResize);
14
+ return () => {
15
+ stdout.off("resize", onResize);
16
+ };
17
+ }, [stdout]);
18
+ return size;
19
+ }
20
+ export function chatContentWidth(cols) {
21
+ return Math.max(20, cols - 12);
22
+ }
@@ -0,0 +1,3 @@
1
+ import type { WriteStream } from "node:tty";
2
+ /** Nudge the terminal viewport toward the latest output (best-effort across terminals). */
3
+ export declare function scrollViewportToBottom(stdout: WriteStream): void;
@@ -0,0 +1,4 @@
1
+ /** Nudge the terminal viewport toward the latest output (best-effort across terminals). */
2
+ export function scrollViewportToBottom(stdout) {
3
+ stdout.write("\x1b[999T");
4
+ }
package/dist/ui/theme.js CHANGED
@@ -19,4 +19,5 @@ export const TOOL_ICONS = {
19
19
  read: "→",
20
20
  write: "⚙",
21
21
  edit: "%",
22
+ web_search: "⌕",
22
23
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devang0907/agent-dev",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Minimal terminal coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",