@devang0907/agent-dev 0.1.3 → 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/README.md +12 -2
- package/dist/agent/loop.d.ts +7 -0
- package/dist/agent/loop.js +97 -42
- package/dist/agent/platform.d.ts +10 -0
- package/dist/agent/platform.js +74 -0
- package/dist/agent/session.d.ts +8 -1
- package/dist/agent/session.js +19 -0
- package/dist/agent/tools/index.d.ts +1 -0
- package/dist/agent/tools/index.js +3 -0
- package/dist/agent/tools/read.js +11 -17
- package/dist/agent/tools/search.d.ts +5 -0
- package/dist/agent/tools/search.js +188 -0
- package/dist/agent/tools/shell.d.ts +1 -0
- package/dist/agent/tools/shell.js +63 -0
- package/dist/modes/print-mode.js +15 -0
- package/dist/providers/openai-compat.d.ts +2 -0
- package/dist/providers/openai-compat.js +95 -0
- package/dist/ui/App.d.ts +1 -0
- package/dist/ui/App.js +60 -15
- package/dist/ui/ChatView.d.ts +2 -1
- package/dist/ui/ChatView.js +14 -4
- package/dist/ui/CommandApprovalPrompt.d.ts +11 -0
- package/dist/ui/CommandApprovalPrompt.js +15 -0
- package/dist/ui/Editor.d.ts +2 -1
- package/dist/ui/Editor.js +6 -2
- package/dist/ui/layout.d.ts +6 -0
- package/dist/ui/layout.js +22 -0
- package/dist/ui/scroll.d.ts +3 -0
- package/dist/ui/scroll.js +4 -0
- package/dist/ui/theme.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# agent-dev
|
|
2
2
|
|
|
3
|
-
A minimal pi-like terminal coding agent with an Ink UI. Chat with an AI that can read, write, edit files, and run
|
|
3
|
+
A minimal pi-like terminal coding agent with an Ink UI. Chat with an AI that can read, write, edit files, search the web, and run shell commands (with your approval).
|
|
4
4
|
|
|
5
5
|
## Quick start
|
|
6
6
|
|
|
@@ -51,7 +51,17 @@ Config and sessions are stored in `~/.agent-dev/`.
|
|
|
51
51
|
|
|
52
52
|
## Tools
|
|
53
53
|
|
|
54
|
-
The agent has
|
|
54
|
+
The agent has five built-in tools:
|
|
55
|
+
|
|
56
|
+
| Tool | Description |
|
|
57
|
+
|------|-------------|
|
|
58
|
+
| `read` | Read a file in the project directory |
|
|
59
|
+
| `write` | Create or overwrite a file |
|
|
60
|
+
| `edit` | Replace text in a file |
|
|
61
|
+
| `web_search` | Search the internet (DuckDuckGo, no extra API key) |
|
|
62
|
+
| `bash` | Run a shell command — **requires your approval** before execution |
|
|
63
|
+
|
|
64
|
+
File operations are restricted to the current working directory. When the agent proposes a shell command, the UI prompts you to approve (`y`) or deny (`n` / Esc).
|
|
55
65
|
|
|
56
66
|
## License
|
|
57
67
|
|
package/dist/agent/loop.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { ChatMessage, Model, ToolCall } from "../providers/types.js";
|
|
2
2
|
import type { Settings } from "../config/settings.js";
|
|
3
|
+
export interface PermissionRequest {
|
|
4
|
+
toolCallId: string;
|
|
5
|
+
name: string;
|
|
6
|
+
args: Record<string, unknown>;
|
|
7
|
+
command: string;
|
|
8
|
+
}
|
|
3
9
|
export type AgentEvent = {
|
|
4
10
|
type: "message_start";
|
|
5
11
|
role: "assistant";
|
|
@@ -28,5 +34,6 @@ export interface AgentLoopOptions {
|
|
|
28
34
|
systemPrompt?: string;
|
|
29
35
|
signal?: AbortSignal;
|
|
30
36
|
onEvent: (event: AgentEvent) => void;
|
|
37
|
+
onPermissionRequest?: (request: PermissionRequest) => Promise<boolean>;
|
|
31
38
|
}
|
|
32
39
|
export declare function runAgentLoop(options: AgentLoopOptions): Promise<ChatMessage[]>;
|
package/dist/agent/loop.js
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
|
-
import { normalizeToolCalls } from "../providers/openai-compat.js";
|
|
1
|
+
import { normalizeToolCalls, parseMalformedToolCalls, extractFailedGeneration, } from "../providers/openai-compat.js";
|
|
2
2
|
import { streamChat } from "../providers/registry.js";
|
|
3
|
-
import { getToolDefinitions, executeTool } from "./tools/index.js";
|
|
3
|
+
import { getToolDefinitions, executeTool, PERMISSION_REQUIRED_TOOLS } from "./tools/index.js";
|
|
4
|
+
import { getPlatformContext } from "./platform.js";
|
|
4
5
|
const MAX_TOOL_ROUNDS = 6;
|
|
5
6
|
const MAX_SAME_TOOL_CALLS = 2;
|
|
6
|
-
const DEFAULT_SYSTEM_PROMPT = `You are a helpful coding assistant with access to tools: read, write, edit, and
|
|
7
|
+
const DEFAULT_SYSTEM_PROMPT = `You are a helpful coding assistant with access to tools: read, write, edit, bash, and web_search.
|
|
7
8
|
When the user asks you to create or modify files, call write or edit once with the full file content, then reply briefly to confirm.
|
|
9
|
+
Use web_search when you need current information, documentation, or facts from the internet.
|
|
10
|
+
Shell commands via bash require user approval before they run — propose the exact command you need.
|
|
11
|
+
Never run long-lived dev servers (npm run dev, npm start) — they time out. Use npm run build to verify, then tell the user how to start the dev server locally.
|
|
8
12
|
Do NOT call the same tool repeatedly with the same arguments. One successful write is enough.
|
|
9
|
-
|
|
13
|
+
When calling tools, use the function-calling API with valid JSON arguments only (e.g. web_search: {"query": "search terms"}).
|
|
14
|
+
|
|
15
|
+
${getPlatformContext()}`;
|
|
16
|
+
function systemPromptForModel(model, base = DEFAULT_SYSTEM_PROMPT) {
|
|
17
|
+
if (model.provider === "groq") {
|
|
18
|
+
return `${base}\nFor Groq: never output <function=...> text — use structured tool calls with JSON arguments.`;
|
|
19
|
+
}
|
|
20
|
+
return base;
|
|
21
|
+
}
|
|
10
22
|
function isToolUseFailedError(message) {
|
|
11
23
|
return /Failed to call a function|tool_use_failed|failed_generation/i.test(message);
|
|
12
24
|
}
|
|
@@ -26,6 +38,74 @@ function dedupeToolCalls(toolCalls) {
|
|
|
26
38
|
return true;
|
|
27
39
|
});
|
|
28
40
|
}
|
|
41
|
+
function resolveToolCalls(content, toolCalls, error) {
|
|
42
|
+
const normalized = dedupeToolCalls(normalizeToolCalls(toolCalls.filter((tc) => tc.name)));
|
|
43
|
+
if (normalized.length > 0)
|
|
44
|
+
return normalized;
|
|
45
|
+
const fromContent = dedupeToolCalls(normalizeToolCalls(parseMalformedToolCalls(content)));
|
|
46
|
+
if (fromContent.length > 0)
|
|
47
|
+
return fromContent;
|
|
48
|
+
if (error) {
|
|
49
|
+
const failed = extractFailedGeneration(error);
|
|
50
|
+
if (failed) {
|
|
51
|
+
return dedupeToolCalls(normalizeToolCalls(parseMalformedToolCalls(failed)));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
async function runToolBatch(uniqueCalls, context, workdir, callCounts, onEvent, onPermissionRequest) {
|
|
57
|
+
let stopAfterBatch = false;
|
|
58
|
+
for (const tc of uniqueCalls) {
|
|
59
|
+
onEvent({ type: "tool_call", toolCall: tc });
|
|
60
|
+
let args = {};
|
|
61
|
+
try {
|
|
62
|
+
args = JSON.parse(tc.arguments || "{}");
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
args = {};
|
|
66
|
+
}
|
|
67
|
+
const sig = toolSignature(tc.name, args);
|
|
68
|
+
const prev = callCounts.get(sig) ?? 0;
|
|
69
|
+
callCounts.set(sig, prev + 1);
|
|
70
|
+
if (prev >= MAX_SAME_TOOL_CALLS) {
|
|
71
|
+
const skip = "Skipped — already executed this action.";
|
|
72
|
+
onEvent({ type: "tool_result", toolCallId: tc.id, name: tc.name, result: skip });
|
|
73
|
+
context.push({ role: "tool", content: skip, toolCallId: tc.id, name: tc.name });
|
|
74
|
+
stopAfterBatch = true;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
let result;
|
|
78
|
+
const needsPermission = PERMISSION_REQUIRED_TOOLS.has(tc.name);
|
|
79
|
+
if (needsPermission && onPermissionRequest) {
|
|
80
|
+
const approved = await onPermissionRequest({
|
|
81
|
+
toolCallId: tc.id,
|
|
82
|
+
name: tc.name,
|
|
83
|
+
args,
|
|
84
|
+
command: String(args.command ?? ""),
|
|
85
|
+
});
|
|
86
|
+
result = approved
|
|
87
|
+
? await executeTool(tc.name, args, workdir)
|
|
88
|
+
: "Command execution denied by user.";
|
|
89
|
+
}
|
|
90
|
+
else if (needsPermission) {
|
|
91
|
+
result = "Command execution denied — permission handler not available.";
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
result = await executeTool(tc.name, args, workdir);
|
|
95
|
+
}
|
|
96
|
+
onEvent({ type: "tool_result", toolCallId: tc.id, name: tc.name, result });
|
|
97
|
+
context.push({
|
|
98
|
+
role: "tool",
|
|
99
|
+
content: result,
|
|
100
|
+
toolCallId: tc.id,
|
|
101
|
+
name: tc.name,
|
|
102
|
+
});
|
|
103
|
+
if (!result.startsWith("Error:") && (tc.name === "write" || tc.name === "edit")) {
|
|
104
|
+
stopAfterBatch = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return stopAfterBatch;
|
|
108
|
+
}
|
|
29
109
|
async function collectStream(model, messages, settings, systemPrompt, signal, onEvent) {
|
|
30
110
|
const tools = getToolDefinitions();
|
|
31
111
|
let content = "";
|
|
@@ -55,7 +135,7 @@ async function collectStream(model, messages, settings, systemPrompt, signal, on
|
|
|
55
135
|
tc.arguments += event.argumentsDelta;
|
|
56
136
|
}
|
|
57
137
|
else if (event.type === "error") {
|
|
58
|
-
return { content, toolCalls:
|
|
138
|
+
return { content, toolCalls: Array.from(toolCallMap.values()), error: event.message };
|
|
59
139
|
}
|
|
60
140
|
}
|
|
61
141
|
const toolCalls = normalizeToolCalls(Array.from(toolCallMap.values()).filter((tc) => tc.name));
|
|
@@ -70,9 +150,10 @@ function finishGracefully(context, content, onEvent) {
|
|
|
70
150
|
onEvent({ type: "turn_end" });
|
|
71
151
|
}
|
|
72
152
|
export async function runAgentLoop(options) {
|
|
73
|
-
const { model, messages, settings, workdir, systemPrompt = DEFAULT_SYSTEM_PROMPT, signal, onEvent, } = options;
|
|
153
|
+
const { model, messages, settings, workdir, systemPrompt = DEFAULT_SYSTEM_PROMPT, signal, onEvent, onPermissionRequest, } = options;
|
|
74
154
|
const context = [...messages];
|
|
75
155
|
const callCounts = new Map();
|
|
156
|
+
const effectivePrompt = systemPromptForModel(model, systemPrompt);
|
|
76
157
|
let toolRound = 0;
|
|
77
158
|
while (true) {
|
|
78
159
|
if (signal?.aborted)
|
|
@@ -83,8 +164,9 @@ export async function runAgentLoop(options) {
|
|
|
83
164
|
break;
|
|
84
165
|
}
|
|
85
166
|
onEvent({ type: "message_start", role: "assistant" });
|
|
86
|
-
const { content, toolCalls, error } = await collectStream(model, context, settings,
|
|
87
|
-
|
|
167
|
+
const { content, toolCalls, error } = await collectStream(model, context, settings, effectivePrompt, signal, onEvent);
|
|
168
|
+
const uniqueCalls = resolveToolCalls(content, toolCalls, error);
|
|
169
|
+
if (error && uniqueCalls.length === 0) {
|
|
88
170
|
if (isToolUseFailedError(error) && hadSuccessfulToolResults(context)) {
|
|
89
171
|
finishGracefully(context, content, onEvent);
|
|
90
172
|
break;
|
|
@@ -92,49 +174,22 @@ export async function runAgentLoop(options) {
|
|
|
92
174
|
onEvent({ type: "error", message: error });
|
|
93
175
|
break;
|
|
94
176
|
}
|
|
95
|
-
const uniqueCalls = dedupeToolCalls(toolCalls);
|
|
96
177
|
const assistantMsg = {
|
|
97
178
|
role: "assistant",
|
|
98
|
-
content,
|
|
179
|
+
content: error ? "" : content,
|
|
99
180
|
toolCalls: uniqueCalls.length > 0 ? uniqueCalls : undefined,
|
|
100
181
|
};
|
|
101
182
|
context.push(assistantMsg);
|
|
102
183
|
if (uniqueCalls.length === 0) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
let stopAfterBatch = false;
|
|
107
|
-
for (const tc of uniqueCalls) {
|
|
108
|
-
onEvent({ type: "tool_call", toolCall: tc });
|
|
109
|
-
let args = {};
|
|
110
|
-
try {
|
|
111
|
-
args = JSON.parse(tc.arguments || "{}");
|
|
184
|
+
if (error) {
|
|
185
|
+
onEvent({ type: "error", message: error });
|
|
112
186
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
const sig = toolSignature(tc.name, args);
|
|
117
|
-
const prev = callCounts.get(sig) ?? 0;
|
|
118
|
-
callCounts.set(sig, prev + 1);
|
|
119
|
-
if (prev >= MAX_SAME_TOOL_CALLS) {
|
|
120
|
-
const skip = "Skipped — already executed this action.";
|
|
121
|
-
onEvent({ type: "tool_result", toolCallId: tc.id, name: tc.name, result: skip });
|
|
122
|
-
context.push({ role: "tool", content: skip, toolCallId: tc.id, name: tc.name });
|
|
123
|
-
stopAfterBatch = true;
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
const result = await executeTool(tc.name, args, workdir);
|
|
127
|
-
onEvent({ type: "tool_result", toolCallId: tc.id, name: tc.name, result });
|
|
128
|
-
context.push({
|
|
129
|
-
role: "tool",
|
|
130
|
-
content: result,
|
|
131
|
-
toolCallId: tc.id,
|
|
132
|
-
name: tc.name,
|
|
133
|
-
});
|
|
134
|
-
if (!result.startsWith("Error:") && (tc.name === "write" || tc.name === "edit")) {
|
|
135
|
-
stopAfterBatch = true;
|
|
187
|
+
else {
|
|
188
|
+
onEvent({ type: "turn_end" });
|
|
136
189
|
}
|
|
190
|
+
break;
|
|
137
191
|
}
|
|
192
|
+
const stopAfterBatch = await runToolBatch(uniqueCalls, context, workdir, callCounts, onEvent, onPermissionRequest);
|
|
138
193
|
if (stopAfterBatch) {
|
|
139
194
|
finishGracefully(context, content, onEvent);
|
|
140
195
|
break;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface ShellConfig {
|
|
2
|
+
executable: string;
|
|
3
|
+
args: string[];
|
|
4
|
+
name: string;
|
|
5
|
+
supportsAndAnd: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function getShellConfig(): ShellConfig;
|
|
8
|
+
/** Adapt common Unix-isms and command chaining for the host shell. */
|
|
9
|
+
export declare function normalizeCommand(command: string, shell: ShellConfig): string;
|
|
10
|
+
export declare function getPlatformContext(): string;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { arch, platform, release } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
function findPwsh() {
|
|
5
|
+
const candidates = [
|
|
6
|
+
process.env.PWSH_PATH,
|
|
7
|
+
join(process.env.ProgramFiles ?? "C:\\Program Files", "PowerShell", "7", "pwsh.exe"),
|
|
8
|
+
join(process.env["ProgramFiles(x86)"] ?? "", "PowerShell", "7", "pwsh.exe"),
|
|
9
|
+
].filter(Boolean);
|
|
10
|
+
for (const candidate of candidates) {
|
|
11
|
+
if (existsSync(candidate))
|
|
12
|
+
return candidate;
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
export function getShellConfig() {
|
|
17
|
+
if (platform() === "win32") {
|
|
18
|
+
const pwsh = findPwsh();
|
|
19
|
+
if (pwsh) {
|
|
20
|
+
return {
|
|
21
|
+
executable: pwsh,
|
|
22
|
+
name: "PowerShell 7+ (pwsh)",
|
|
23
|
+
supportsAndAnd: true,
|
|
24
|
+
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command"],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
executable: "powershell.exe",
|
|
29
|
+
name: "Windows PowerShell",
|
|
30
|
+
supportsAndAnd: false,
|
|
31
|
+
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command"],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
executable: process.env.SHELL ?? "/bin/bash",
|
|
36
|
+
name: "bash",
|
|
37
|
+
supportsAndAnd: true,
|
|
38
|
+
args: ["-lc"],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Adapt common Unix-isms and command chaining for the host shell. */
|
|
42
|
+
export function normalizeCommand(command, shell) {
|
|
43
|
+
let cmd = command.trim();
|
|
44
|
+
if (!cmd)
|
|
45
|
+
return cmd;
|
|
46
|
+
if (platform() === "win32") {
|
|
47
|
+
if (!shell.supportsAndAnd) {
|
|
48
|
+
cmd = cmd.replace(/\s&&\s/g, "; ");
|
|
49
|
+
}
|
|
50
|
+
cmd = cmd
|
|
51
|
+
.replace(/\bmkdir -p\s+/g, "New-Item -ItemType Directory -Force -Path ")
|
|
52
|
+
.replace(/\brm -rf\s+/g, "Remove-Item -Recurse -Force ")
|
|
53
|
+
.replace(/\brm -r\s+/g, "Remove-Item -Recurse -Force ")
|
|
54
|
+
.replace(/\btouch\s+/g, "New-Item -ItemType File -Force ");
|
|
55
|
+
}
|
|
56
|
+
return cmd;
|
|
57
|
+
}
|
|
58
|
+
export function getPlatformContext() {
|
|
59
|
+
const shell = getShellConfig();
|
|
60
|
+
const lines = [
|
|
61
|
+
`Platform: ${platform()} ${arch()} (${release()})`,
|
|
62
|
+
`Shell: ${shell.name}`,
|
|
63
|
+
`Working directory: ${process.cwd()}`,
|
|
64
|
+
];
|
|
65
|
+
if (platform() === "win32") {
|
|
66
|
+
lines.push("This agent runs on Windows. Use PowerShell-compatible commands.", shell.supportsAndAnd
|
|
67
|
+
? "You may chain commands with ; or &&."
|
|
68
|
+
: "Chain commands with ; (&& is NOT supported in Windows PowerShell 5).", "Examples: New-Item -ItemType Directory -Force todo-app; Set-Location todo-app", "For npx/npm scaffolding, always use non-interactive flags (--yes, -y, --defaults) and set CI=1.", "Do not use mkdir -p, rm -rf, or touch — use PowerShell equivalents or the write tool.", "Never run dev servers (npm run dev, npm start, next dev) via bash — they run forever and will time out.", "To verify a web app, use npm run build. Tell the user to run the dev server in their own terminal.", "Do not run npm audit fix unless the user explicitly asks.");
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
lines.push("Use bash/sh syntax. Chain commands with && or ;.", "For npx/npm scaffolding, use non-interactive flags (--yes, -y) to avoid prompts.", "Never run dev servers (npm run dev, npm start) via bash — they run forever.", "To verify a web app, use npm run build. Tell the user to run the dev server separately.");
|
|
72
|
+
}
|
|
73
|
+
return lines.join("\n");
|
|
74
|
+
}
|
package/dist/agent/session.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import type { ChatMessage, Model } from "../providers/types.js";
|
|
3
3
|
import type { Settings } from "../config/settings.js";
|
|
4
|
-
import { type AgentEvent } from "./loop.js";
|
|
4
|
+
import { type AgentEvent, type PermissionRequest } from "./loop.js";
|
|
5
5
|
import { SessionManager } from "../session/manager.js";
|
|
6
6
|
export type SessionEvent = AgentEvent | {
|
|
7
7
|
type: "user_message";
|
|
@@ -9,6 +9,9 @@ export type SessionEvent = AgentEvent | {
|
|
|
9
9
|
} | {
|
|
10
10
|
type: "model_changed";
|
|
11
11
|
model: Model;
|
|
12
|
+
} | {
|
|
13
|
+
type: "permission_request";
|
|
14
|
+
request: PermissionRequest;
|
|
12
15
|
};
|
|
13
16
|
export declare class AgentSession extends EventEmitter {
|
|
14
17
|
private messages;
|
|
@@ -18,6 +21,7 @@ export declare class AgentSession extends EventEmitter {
|
|
|
18
21
|
private sessionManager;
|
|
19
22
|
private abortController?;
|
|
20
23
|
private running;
|
|
24
|
+
private pendingPermission?;
|
|
21
25
|
constructor(settings: Settings, sessionManager: SessionManager, workdir: string, initialModel?: Model);
|
|
22
26
|
getModel(): Model;
|
|
23
27
|
getSettings(): Settings;
|
|
@@ -26,6 +30,9 @@ export declare class AgentSession extends EventEmitter {
|
|
|
26
30
|
setModel(model: Model): void;
|
|
27
31
|
updateSettings(settings: Settings): void;
|
|
28
32
|
abort(): void;
|
|
33
|
+
respondToPermission(approved: boolean): void;
|
|
34
|
+
private resolvePermission;
|
|
35
|
+
private requestCommandPermission;
|
|
29
36
|
prompt(content: string): Promise<void>;
|
|
30
37
|
newSession(): void;
|
|
31
38
|
getAvailableModels(): Model[];
|
package/dist/agent/session.js
CHANGED
|
@@ -11,6 +11,7 @@ export class AgentSession extends EventEmitter {
|
|
|
11
11
|
sessionManager;
|
|
12
12
|
abortController;
|
|
13
13
|
running = false;
|
|
14
|
+
pendingPermission;
|
|
14
15
|
constructor(settings, sessionManager, workdir, initialModel) {
|
|
15
16
|
super();
|
|
16
17
|
this.settings = settings;
|
|
@@ -49,6 +50,23 @@ export class AgentSession extends EventEmitter {
|
|
|
49
50
|
}
|
|
50
51
|
abort() {
|
|
51
52
|
this.abortController?.abort();
|
|
53
|
+
this.resolvePermission(false);
|
|
54
|
+
}
|
|
55
|
+
respondToPermission(approved) {
|
|
56
|
+
this.resolvePermission(approved);
|
|
57
|
+
}
|
|
58
|
+
resolvePermission(approved) {
|
|
59
|
+
const resolve = this.pendingPermission;
|
|
60
|
+
if (resolve) {
|
|
61
|
+
this.pendingPermission = undefined;
|
|
62
|
+
resolve(approved);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
requestCommandPermission(request) {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
this.pendingPermission = resolve;
|
|
68
|
+
this.emit("event", { type: "permission_request", request });
|
|
69
|
+
});
|
|
52
70
|
}
|
|
53
71
|
async prompt(content) {
|
|
54
72
|
if (this.running)
|
|
@@ -67,6 +85,7 @@ export class AgentSession extends EventEmitter {
|
|
|
67
85
|
workdir: this.workdir,
|
|
68
86
|
signal: this.abortController.signal,
|
|
69
87
|
onEvent: (event) => this.emit("event", event),
|
|
88
|
+
onPermissionRequest: (request) => this.requestCommandPermission(request),
|
|
70
89
|
});
|
|
71
90
|
for (const msg of newMessages) {
|
|
72
91
|
if (msg.role !== "user") {
|
|
@@ -4,5 +4,6 @@ export interface AgentTool {
|
|
|
4
4
|
execute: (args: Record<string, unknown>, workdir: string) => Promise<string>;
|
|
5
5
|
}
|
|
6
6
|
export declare const BUILTIN_TOOLS: AgentTool[];
|
|
7
|
+
export declare const PERMISSION_REQUIRED_TOOLS: Set<string>;
|
|
7
8
|
export declare function getToolDefinitions(): ToolDefinition[];
|
|
8
9
|
export declare function executeTool(name: string, args: Record<string, unknown>, workdir: string): Promise<string>;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { readTool, writeTool, editTool, bashTool, executeRead, executeWrite, executeEdit, executeBash, } from "./read.js";
|
|
2
|
+
import { webSearchTool, executeWebSearch } from "./search.js";
|
|
2
3
|
export const BUILTIN_TOOLS = [
|
|
3
4
|
{ definition: readTool, execute: (args, wd) => executeRead(args, wd) },
|
|
4
5
|
{ definition: writeTool, execute: (args, wd) => executeWrite(args, wd) },
|
|
5
6
|
{ definition: editTool, execute: (args, wd) => executeEdit(args, wd) },
|
|
6
7
|
{ definition: bashTool, execute: (args, wd) => executeBash(args, wd) },
|
|
8
|
+
{ definition: webSearchTool, execute: (args) => executeWebSearch(args) },
|
|
7
9
|
];
|
|
10
|
+
export const PERMISSION_REQUIRED_TOOLS = new Set(["bash"]);
|
|
8
11
|
export function getToolDefinitions() {
|
|
9
12
|
return BUILTIN_TOOLS.map((t) => t.definition);
|
|
10
13
|
}
|
package/dist/agent/tools/read.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
2
|
import { resolve, isAbsolute } from "node:path";
|
|
3
|
+
import { platform as osPlatform } from "node:os";
|
|
4
|
+
import { getShellConfig } from "../platform.js";
|
|
5
|
+
import { executeShellCommand } from "./shell.js";
|
|
3
6
|
const DEFAULT_WORKDIR = process.cwd();
|
|
4
7
|
function resolvePath(path, workdir = DEFAULT_WORKDIR) {
|
|
5
8
|
return isAbsolute(path) ? path : resolve(workdir, path);
|
|
@@ -81,7 +84,9 @@ export async function executeEdit(args, workdir = DEFAULT_WORKDIR) {
|
|
|
81
84
|
}
|
|
82
85
|
export const bashTool = {
|
|
83
86
|
name: "bash",
|
|
84
|
-
description:
|
|
87
|
+
description: osPlatform() === "win32"
|
|
88
|
+
? "Run a PowerShell command in the project directory (Windows). Chain with ; not &&. Use non-interactive flags for npx/npm."
|
|
89
|
+
: "Run a bash shell command in the project directory",
|
|
85
90
|
parameters: {
|
|
86
91
|
type: "object",
|
|
87
92
|
properties: {
|
|
@@ -92,21 +97,10 @@ export const bashTool = {
|
|
|
92
97
|
},
|
|
93
98
|
};
|
|
94
99
|
export async function executeBash(args, workdir = DEFAULT_WORKDIR) {
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const { stdout, stderr } = await execAsync(args.command, {
|
|
100
|
-
cwd: workdir,
|
|
101
|
-
timeout: 120000,
|
|
102
|
-
maxBuffer: 1024 * 1024,
|
|
103
|
-
});
|
|
104
|
-
const out = stdout + (stderr ? `\n${stderr}` : "");
|
|
105
|
-
return out.trim() || "(no output)";
|
|
106
|
-
}
|
|
107
|
-
catch (err) {
|
|
108
|
-
const e = err;
|
|
109
|
-
const out = (e.stdout ?? "") + (e.stderr ? `\n${e.stderr}` : "");
|
|
110
|
-
return out.trim() || (e.message ?? "Command failed");
|
|
100
|
+
const shell = getShellConfig();
|
|
101
|
+
const result = await executeShellCommand(args.command, workdir);
|
|
102
|
+
if (result.startsWith("Error:")) {
|
|
103
|
+
return `${result}\n(shell: ${shell.name})`;
|
|
111
104
|
}
|
|
105
|
+
return result;
|
|
112
106
|
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
export const webSearchTool = {
|
|
2
|
+
name: "web_search",
|
|
3
|
+
description: "Search the internet for current information, documentation, or news",
|
|
4
|
+
parameters: {
|
|
5
|
+
type: "object",
|
|
6
|
+
properties: {
|
|
7
|
+
query: { type: "string", description: "Search query" },
|
|
8
|
+
},
|
|
9
|
+
required: ["query"],
|
|
10
|
+
additionalProperties: false,
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
14
|
+
function stripHtml(text) {
|
|
15
|
+
return decodeHtmlEntities(text.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim());
|
|
16
|
+
}
|
|
17
|
+
function decodeHtmlEntities(text) {
|
|
18
|
+
return text
|
|
19
|
+
.replace(/&/g, "&")
|
|
20
|
+
.replace(/</g, "<")
|
|
21
|
+
.replace(/>/g, ">")
|
|
22
|
+
.replace(/"/g, '"')
|
|
23
|
+
.replace(/'/g, "'")
|
|
24
|
+
.replace(/'/g, "'")
|
|
25
|
+
.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)));
|
|
26
|
+
}
|
|
27
|
+
function extractAttr(tag, attr) {
|
|
28
|
+
const re = new RegExp(`${attr}=['"]([^'"]*)['"]`, "i");
|
|
29
|
+
return tag.match(re)?.[1] ?? null;
|
|
30
|
+
}
|
|
31
|
+
function decodeDdgUrl(href) {
|
|
32
|
+
try {
|
|
33
|
+
const match = href.match(/uddg=([^&]+)/);
|
|
34
|
+
if (match)
|
|
35
|
+
return decodeURIComponent(match[1]);
|
|
36
|
+
if (href.startsWith("//"))
|
|
37
|
+
return `https:${href}`;
|
|
38
|
+
return href;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return href;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function parseTagsByClass(html, className) {
|
|
45
|
+
const re = new RegExp(`<a\\b[^>]*class=['"]${className}['"][^>]*>[\\s\\S]*?<\\/a>`, "gi");
|
|
46
|
+
return html.match(re) ?? [];
|
|
47
|
+
}
|
|
48
|
+
function parseDdgLiteHtml(html) {
|
|
49
|
+
const linkTags = parseTagsByClass(html, "result-link");
|
|
50
|
+
const snippetRe = /<td[^>]*class=['"]result-snippet['"][^>]*>([\s\S]*?)<\/td>/gi;
|
|
51
|
+
const snippets = [];
|
|
52
|
+
let match;
|
|
53
|
+
while ((match = snippetRe.exec(html)) !== null) {
|
|
54
|
+
snippets.push(stripHtml(match[1]));
|
|
55
|
+
}
|
|
56
|
+
const results = [];
|
|
57
|
+
for (let i = 0; i < linkTags.length && results.length < 8; i++) {
|
|
58
|
+
const tag = linkTags[i];
|
|
59
|
+
const href = extractAttr(tag, "href");
|
|
60
|
+
const title = stripHtml(tag.replace(/^<a\b[^>]*>/i, "").replace(/<\/a>$/i, ""));
|
|
61
|
+
if (href && title) {
|
|
62
|
+
results.push({
|
|
63
|
+
title,
|
|
64
|
+
url: decodeDdgUrl(href),
|
|
65
|
+
snippet: snippets[i] ?? "",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return results;
|
|
70
|
+
}
|
|
71
|
+
function parseDdgHtmlResults(html) {
|
|
72
|
+
const linkTags = parseTagsByClass(html, "result__a");
|
|
73
|
+
const snippetTags = parseTagsByClass(html, "result__snippet");
|
|
74
|
+
const results = [];
|
|
75
|
+
for (let i = 0; i < linkTags.length && results.length < 8; i++) {
|
|
76
|
+
const tag = linkTags[i];
|
|
77
|
+
const href = extractAttr(tag, "href");
|
|
78
|
+
const title = stripHtml(tag.replace(/^<a\b[^>]*>/i, "").replace(/<\/a>$/i, ""));
|
|
79
|
+
const snippetTag = snippetTags[i];
|
|
80
|
+
const snippet = snippetTag
|
|
81
|
+
? stripHtml(snippetTag.replace(/^<a\b[^>]*>/i, "").replace(/<\/a>$/i, ""))
|
|
82
|
+
: "";
|
|
83
|
+
if (href && title) {
|
|
84
|
+
results.push({ title, url: decodeDdgUrl(href), snippet });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return results;
|
|
88
|
+
}
|
|
89
|
+
async function fetchDdgLiteResults(query) {
|
|
90
|
+
const res = await fetch("https://lite.duckduckgo.com/lite/", {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: {
|
|
93
|
+
"User-Agent": USER_AGENT,
|
|
94
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
95
|
+
},
|
|
96
|
+
body: new URLSearchParams({ q: query }),
|
|
97
|
+
signal: AbortSignal.timeout(15000),
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok)
|
|
100
|
+
throw new Error(`DuckDuckGo lite failed (${res.status})`);
|
|
101
|
+
return parseDdgLiteHtml(await res.text());
|
|
102
|
+
}
|
|
103
|
+
async function fetchDdgHtmlResults(query) {
|
|
104
|
+
const res = await fetch("https://html.duckduckgo.com/html/", {
|
|
105
|
+
method: "POST",
|
|
106
|
+
headers: {
|
|
107
|
+
"User-Agent": USER_AGENT,
|
|
108
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
109
|
+
},
|
|
110
|
+
body: new URLSearchParams({ q: query, b: "", kl: "" }),
|
|
111
|
+
signal: AbortSignal.timeout(15000),
|
|
112
|
+
});
|
|
113
|
+
if (!res.ok)
|
|
114
|
+
throw new Error(`DuckDuckGo html failed (${res.status})`);
|
|
115
|
+
return parseDdgHtmlResults(await res.text());
|
|
116
|
+
}
|
|
117
|
+
async function fetchWebResults(query) {
|
|
118
|
+
const lite = await fetchDdgLiteResults(query);
|
|
119
|
+
if (lite.length > 0)
|
|
120
|
+
return lite;
|
|
121
|
+
return fetchDdgHtmlResults(query);
|
|
122
|
+
}
|
|
123
|
+
async function fetchDdgInstantAnswer(query) {
|
|
124
|
+
const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_redirect=1&no_html=1`;
|
|
125
|
+
const res = await fetch(url, {
|
|
126
|
+
headers: { "User-Agent": "agent-dev/1.0" },
|
|
127
|
+
signal: AbortSignal.timeout(10000),
|
|
128
|
+
});
|
|
129
|
+
if (!res.ok)
|
|
130
|
+
return null;
|
|
131
|
+
const data = (await res.json());
|
|
132
|
+
const parts = [];
|
|
133
|
+
if (data.AbstractText) {
|
|
134
|
+
parts.push(data.Heading ? `${data.Heading}: ${data.AbstractText}` : data.AbstractText);
|
|
135
|
+
if (data.AbstractURL)
|
|
136
|
+
parts.push(`Source: ${data.AbstractURL}`);
|
|
137
|
+
}
|
|
138
|
+
for (const topic of data.RelatedTopics ?? []) {
|
|
139
|
+
if ("Text" in topic && topic.Text) {
|
|
140
|
+
parts.push(topic.Text);
|
|
141
|
+
if (topic.FirstURL)
|
|
142
|
+
parts.push(` ${topic.FirstURL}`);
|
|
143
|
+
}
|
|
144
|
+
else if ("Topics" in topic && topic.Topics) {
|
|
145
|
+
for (const sub of topic.Topics) {
|
|
146
|
+
if (sub.Text)
|
|
147
|
+
parts.push(sub.Text);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (parts.length >= 6)
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
return parts.length > 0 ? parts.join("\n") : null;
|
|
154
|
+
}
|
|
155
|
+
function formatResults(query, instant, web) {
|
|
156
|
+
const lines = [`Search results for: ${query}`, ""];
|
|
157
|
+
if (instant) {
|
|
158
|
+
lines.push("Instant answer:", instant, "");
|
|
159
|
+
}
|
|
160
|
+
if (web.length === 0) {
|
|
161
|
+
lines.push(instant ? "(No additional web results.)" : "No results found.");
|
|
162
|
+
return lines.join("\n");
|
|
163
|
+
}
|
|
164
|
+
lines.push("Web results:");
|
|
165
|
+
for (const [i, r] of web.entries()) {
|
|
166
|
+
lines.push(`${i + 1}. ${r.title}`);
|
|
167
|
+
lines.push(` ${r.url}`);
|
|
168
|
+
if (r.snippet)
|
|
169
|
+
lines.push(` ${r.snippet}`);
|
|
170
|
+
lines.push("");
|
|
171
|
+
}
|
|
172
|
+
return lines.join("\n").trim();
|
|
173
|
+
}
|
|
174
|
+
export async function executeWebSearch(args) {
|
|
175
|
+
const query = args.query?.trim();
|
|
176
|
+
if (!query)
|
|
177
|
+
return "Error: query is required";
|
|
178
|
+
try {
|
|
179
|
+
const [instant, web] = await Promise.all([
|
|
180
|
+
fetchDdgInstantAnswer(query).catch(() => null),
|
|
181
|
+
fetchWebResults(query),
|
|
182
|
+
]);
|
|
183
|
+
return formatResults(query, instant, web);
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
return `Error: search failed — ${err instanceof Error ? err.message : String(err)}`;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function executeShellCommand(command: string, workdir: string): Promise<string>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { getShellConfig, normalizeCommand } from "../platform.js";
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
4
|
+
const INSTALL_TIMEOUT_MS = 300_000;
|
|
5
|
+
const DEV_SERVER_RE = /\b(npm run dev|npm start|yarn dev|pnpm dev|next dev|nuxt dev|vite(\s|$)|react-scripts start|node .*--watch)\b/i;
|
|
6
|
+
const INSTALL_RE = /\b(npm install|npm ci|yarn install|pnpm install|npx create-|npm audit)\b/i;
|
|
7
|
+
function getCommandTimeout(command) {
|
|
8
|
+
return INSTALL_RE.test(command) ? INSTALL_TIMEOUT_MS : DEFAULT_TIMEOUT_MS;
|
|
9
|
+
}
|
|
10
|
+
function isDevServerCommand(command) {
|
|
11
|
+
return DEV_SERVER_RE.test(command) || /\brun dev\b/i.test(command);
|
|
12
|
+
}
|
|
13
|
+
export async function executeShellCommand(command, workdir) {
|
|
14
|
+
const shell = getShellConfig();
|
|
15
|
+
const normalized = normalizeCommand(command, shell);
|
|
16
|
+
if (isDevServerCommand(normalized)) {
|
|
17
|
+
return [
|
|
18
|
+
"Error: dev servers run until stopped and cannot run inside the agent.",
|
|
19
|
+
`Suggested: open a separate terminal, cd to the project, then run: ${normalized.trim()}`,
|
|
20
|
+
"To verify the project here, use a one-shot command like: npm run build",
|
|
21
|
+
].join("\n");
|
|
22
|
+
}
|
|
23
|
+
const timeoutMs = getCommandTimeout(normalized);
|
|
24
|
+
const args = [...shell.args, normalized];
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
let stdout = "";
|
|
27
|
+
let stderr = "";
|
|
28
|
+
const child = spawn(shell.executable, args, {
|
|
29
|
+
cwd: workdir,
|
|
30
|
+
env: {
|
|
31
|
+
...process.env,
|
|
32
|
+
CI: "1",
|
|
33
|
+
FORCE_COLOR: "0",
|
|
34
|
+
npm_config_yes: "true",
|
|
35
|
+
},
|
|
36
|
+
windowsHide: true,
|
|
37
|
+
});
|
|
38
|
+
const timer = setTimeout(() => {
|
|
39
|
+
child.kill();
|
|
40
|
+
resolve(`Error: command timed out after ${timeoutMs / 1000}s\n${combineOutput(stdout, stderr)}\nTip: dev servers (npm run dev) cannot run here — use npm run build to verify, or run the dev server in your own terminal.`.trim());
|
|
41
|
+
}, timeoutMs);
|
|
42
|
+
child.stdout?.on("data", (chunk) => {
|
|
43
|
+
stdout += String(chunk);
|
|
44
|
+
});
|
|
45
|
+
child.stderr?.on("data", (chunk) => {
|
|
46
|
+
stderr += String(chunk);
|
|
47
|
+
});
|
|
48
|
+
child.on("error", (err) => {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
resolve(`Error: ${err.message}`);
|
|
51
|
+
});
|
|
52
|
+
child.on("close", (code) => {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
const out = combineOutput(stdout, stderr);
|
|
55
|
+
if (code === 0)
|
|
56
|
+
return resolve(out.trim() || "(no output)");
|
|
57
|
+
resolve(out.trim() ? `${out.trim()}\n(exit ${code})` : `Error: command failed (exit ${code})`);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function combineOutput(stdout, stderr) {
|
|
62
|
+
return stdout + (stderr ? (stdout ? `\n${stderr}` : stderr) : "");
|
|
63
|
+
}
|
package/dist/modes/print-mode.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
2
4
|
import { runAgentLoop } from "../agent/loop.js";
|
|
5
|
+
async function promptCommandApproval(request) {
|
|
6
|
+
console.log(chalk.yellow(`\nCommand approval required:`));
|
|
7
|
+
console.log(chalk.white(` ${request.command}`));
|
|
8
|
+
const rl = createInterface({ input, output });
|
|
9
|
+
try {
|
|
10
|
+
const answer = await rl.question(chalk.gray("Run? [y/N] "));
|
|
11
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
rl.close();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
3
17
|
export async function runPrintMode(session, prompt) {
|
|
4
18
|
const model = session.getModel();
|
|
5
19
|
console.log(chalk.gray(`Model: ${model.provider}/${model.id}`));
|
|
@@ -11,6 +25,7 @@ export async function runPrintMode(session, prompt) {
|
|
|
11
25
|
messages: [...prior, userMsg],
|
|
12
26
|
settings: session.getSettings(),
|
|
13
27
|
workdir: process.cwd(),
|
|
28
|
+
onPermissionRequest: promptCommandApproval,
|
|
14
29
|
onEvent: (event) => {
|
|
15
30
|
if (event.type === "text_delta") {
|
|
16
31
|
process.stdout.write(event.delta);
|
|
@@ -3,6 +3,8 @@ import type { ChatContext, StreamEvent, ToolCall } from "./types.js";
|
|
|
3
3
|
export declare function sanitizeToolParameters(params: Record<string, unknown>): Record<string, unknown>;
|
|
4
4
|
export declare function toOpenAITools(tools: ChatContext["tools"]): OpenAI.Chat.ChatCompletionTool[];
|
|
5
5
|
export declare function normalizeToolCalls(toolCalls: ToolCall[]): ToolCall[];
|
|
6
|
+
export declare function parseMalformedToolCalls(text: string): ToolCall[];
|
|
7
|
+
export declare function extractFailedGeneration(errorMessage: string): string | null;
|
|
6
8
|
export declare function toOpenAIMessages(ctx: ChatContext): OpenAI.Chat.ChatCompletionMessageParam[];
|
|
7
9
|
export declare function processOpenAIStream(stream: AsyncIterable<OpenAI.Chat.Completions.ChatCompletionChunk>): AsyncGenerator<StreamEvent>;
|
|
8
10
|
export declare function formatApiError(err: unknown): string;
|
|
@@ -22,6 +22,101 @@ export function normalizeToolCalls(toolCalls) {
|
|
|
22
22
|
arguments: tc.arguments?.trim() || "{}",
|
|
23
23
|
}));
|
|
24
24
|
}
|
|
25
|
+
/** Recover tool calls Groq/Llama sometimes emit as malformed text instead of structured tool_calls. */
|
|
26
|
+
function unescapeJsonString(value) {
|
|
27
|
+
return value
|
|
28
|
+
.replace(/\\n/g, "\n")
|
|
29
|
+
.replace(/\\t/g, "\t")
|
|
30
|
+
.replace(/\\"/g, '"')
|
|
31
|
+
.replace(/\\\\/g, "\\");
|
|
32
|
+
}
|
|
33
|
+
function parseToolArguments(raw) {
|
|
34
|
+
const body = raw.replace(/^[\[\]\s]*/, "").trim();
|
|
35
|
+
if (!body.startsWith("{"))
|
|
36
|
+
return null;
|
|
37
|
+
try {
|
|
38
|
+
JSON.parse(body);
|
|
39
|
+
return body;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Groq often truncates JSON — extract known fields.
|
|
43
|
+
}
|
|
44
|
+
const commandMatch = body.match(/"command"\s*:\s*"((?:\\.|[^"\\])*)"/);
|
|
45
|
+
if (commandMatch) {
|
|
46
|
+
return JSON.stringify({ command: unescapeJsonString(commandMatch[1]) });
|
|
47
|
+
}
|
|
48
|
+
const truncatedCommand = body.match(/"command"\s*:\s*"([\s\S]+)$/);
|
|
49
|
+
if (truncatedCommand) {
|
|
50
|
+
const command = unescapeJsonString(truncatedCommand[1].replace(/\\+$/, ""));
|
|
51
|
+
return JSON.stringify({ command });
|
|
52
|
+
}
|
|
53
|
+
const queryMatch = body.match(/"query"\s*:\s*"((?:\\.|[^"\\])*)"/);
|
|
54
|
+
if (queryMatch) {
|
|
55
|
+
return JSON.stringify({ query: unescapeJsonString(queryMatch[1]) });
|
|
56
|
+
}
|
|
57
|
+
const pathMatch = body.match(/"path"\s*:\s*"((?:\\.|[^"\\])*)"/);
|
|
58
|
+
if (pathMatch) {
|
|
59
|
+
return JSON.stringify({ path: unescapeJsonString(pathMatch[1]) });
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function pushRecovered(results, name, args) {
|
|
64
|
+
results.push({
|
|
65
|
+
id: `recovered_${Date.now()}_${results.length}`,
|
|
66
|
+
name,
|
|
67
|
+
arguments: args,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
export function parseMalformedToolCalls(text) {
|
|
71
|
+
const results = [];
|
|
72
|
+
if (!text)
|
|
73
|
+
return results;
|
|
74
|
+
const patterns = [
|
|
75
|
+
/<function=([a-zA-Z0-9_]+)\s*(?:\[\])?\s*(\{[\s\S]*?\})\s*<\/function>/gi,
|
|
76
|
+
/<function=([a-zA-Z0-9_]+)\s*>\s*(\{[\s\S]*?\})\s*<\/function>/gi,
|
|
77
|
+
/<tool_call>\s*([a-zA-Z0-9_]+)\s*(\{[\s\S]*?\})\s*<\/tool_call>/gi,
|
|
78
|
+
];
|
|
79
|
+
for (const re of patterns) {
|
|
80
|
+
re.lastIndex = 0;
|
|
81
|
+
let match;
|
|
82
|
+
while ((match = re.exec(text)) !== null) {
|
|
83
|
+
const name = match[1].trim();
|
|
84
|
+
const args = parseToolArguments(match[2]);
|
|
85
|
+
if (name && args)
|
|
86
|
+
pushRecovered(results, name, args);
|
|
87
|
+
}
|
|
88
|
+
if (results.length > 0)
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
if (results.length === 0) {
|
|
92
|
+
const looseRe = /<function=([a-zA-Z0-9_]+)([\s\S]*?)<\/function>/gi;
|
|
93
|
+
let match;
|
|
94
|
+
while ((match = looseRe.exec(text)) !== null) {
|
|
95
|
+
const name = match[1].trim();
|
|
96
|
+
const args = parseToolArguments(match[2]);
|
|
97
|
+
if (name && args)
|
|
98
|
+
pushRecovered(results, name, args);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (results.length === 0) {
|
|
102
|
+
const truncatedRe = /<function=([a-zA-Z0-9_]+)\s*(?:\[\])?\s*(\{[\s\S]+)/gi;
|
|
103
|
+
let match;
|
|
104
|
+
while ((match = truncatedRe.exec(text)) !== null) {
|
|
105
|
+
const name = match[1].trim();
|
|
106
|
+
const args = parseToolArguments(match[2]);
|
|
107
|
+
if (name && args)
|
|
108
|
+
pushRecovered(results, name, args);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return results;
|
|
112
|
+
}
|
|
113
|
+
export function extractFailedGeneration(errorMessage) {
|
|
114
|
+
const marker = "Model output:";
|
|
115
|
+
const idx = errorMessage.indexOf(marker);
|
|
116
|
+
if (idx >= 0)
|
|
117
|
+
return errorMessage.slice(idx + marker.length).trim();
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
25
120
|
export function toOpenAIMessages(ctx) {
|
|
26
121
|
const msgs = [];
|
|
27
122
|
if (ctx.systemPrompt) {
|
package/dist/ui/App.d.ts
CHANGED
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,14 @@ 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
|
+
let nextMessageId = 0;
|
|
17
|
+
function toDisplayMessage(role, content, toolName) {
|
|
18
|
+
return { id: nextMessageId++, role, content, toolName };
|
|
19
|
+
}
|
|
14
20
|
function modelForProvider(provider, settings) {
|
|
15
21
|
const current = findModel(settings.defaultProvider, settings.defaultModel);
|
|
16
22
|
if (current?.provider === provider)
|
|
@@ -19,11 +25,7 @@ function modelForProvider(provider, settings) {
|
|
|
19
25
|
}
|
|
20
26
|
export function App({ session, workdir, onQuit }) {
|
|
21
27
|
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
|
-
})));
|
|
28
|
+
const [displayMessages, setDisplayMessages] = useState(() => session.getMessages().map((m) => toDisplayMessage(m.role === "tool" ? "tool" : m.role === "user" ? "user" : "assistant", m.content, m.name)));
|
|
27
29
|
const [streamingText, setStreamingText] = useState("");
|
|
28
30
|
const [overlay, setOverlay] = useState("none");
|
|
29
31
|
const [modelFilter, setModelFilter] = useState();
|
|
@@ -32,9 +34,13 @@ export function App({ session, workdir, onQuit }) {
|
|
|
32
34
|
const [settings, setSettings] = useState(session.getSettings());
|
|
33
35
|
const [model, setModel] = useState(session.getModel());
|
|
34
36
|
const [running, setRunning] = useState(false);
|
|
37
|
+
const [autoFollow, setAutoFollow] = useState(true);
|
|
38
|
+
const [pendingCommand, setPendingCommand] = useState(null);
|
|
35
39
|
const streamingRef = useRef("");
|
|
40
|
+
const autoFollowRef = useRef(true);
|
|
36
41
|
const startupChecked = useRef(false);
|
|
37
42
|
const theme = getTheme();
|
|
43
|
+
const { stdout } = useStdout();
|
|
38
44
|
const openApiKeyPrompt = useCallback((target, returnTo = "none") => {
|
|
39
45
|
setPendingModel(target);
|
|
40
46
|
setApiKeyReturnOverlay(returnTo);
|
|
@@ -65,18 +71,28 @@ export function App({ session, workdir, onQuit }) {
|
|
|
65
71
|
openApiKeyPrompt(current, "none");
|
|
66
72
|
}
|
|
67
73
|
}, [session, settings, openApiKeyPrompt]);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (autoFollow && streamingText) {
|
|
76
|
+
scrollViewportToBottom(stdout);
|
|
77
|
+
}
|
|
78
|
+
}, [streamingText, autoFollow, stdout]);
|
|
68
79
|
useEffect(() => {
|
|
69
80
|
const handler = (event) => {
|
|
70
81
|
switch (event.type) {
|
|
71
82
|
case "user_message":
|
|
72
|
-
|
|
83
|
+
autoFollowRef.current = true;
|
|
84
|
+
setAutoFollow(true);
|
|
85
|
+
setDisplayMessages((prev) => [...prev, toDisplayMessage("user", event.content)]);
|
|
73
86
|
setRunning(true);
|
|
74
87
|
streamingRef.current = "";
|
|
75
88
|
setStreamingText("");
|
|
89
|
+
scrollViewportToBottom(stdout);
|
|
76
90
|
break;
|
|
77
91
|
case "message_start":
|
|
78
92
|
streamingRef.current = "";
|
|
79
93
|
setStreamingText("");
|
|
94
|
+
if (autoFollowRef.current)
|
|
95
|
+
scrollViewportToBottom(stdout);
|
|
80
96
|
break;
|
|
81
97
|
case "text_delta":
|
|
82
98
|
streamingRef.current += event.delta;
|
|
@@ -85,7 +101,7 @@ export function App({ session, workdir, onQuit }) {
|
|
|
85
101
|
case "tool_call":
|
|
86
102
|
const partial = streamingRef.current;
|
|
87
103
|
if (partial) {
|
|
88
|
-
setDisplayMessages((prev) => [...prev,
|
|
104
|
+
setDisplayMessages((prev) => [...prev, toDisplayMessage("assistant", partial)]);
|
|
89
105
|
streamingRef.current = "";
|
|
90
106
|
setStreamingText("");
|
|
91
107
|
}
|
|
@@ -93,13 +109,13 @@ export function App({ session, workdir, onQuit }) {
|
|
|
93
109
|
case "tool_result":
|
|
94
110
|
setDisplayMessages((prev) => [
|
|
95
111
|
...prev,
|
|
96
|
-
|
|
112
|
+
toDisplayMessage("tool", event.result, event.name),
|
|
97
113
|
]);
|
|
98
114
|
break;
|
|
99
115
|
case "turn_end":
|
|
100
116
|
const final = streamingRef.current;
|
|
101
117
|
if (final) {
|
|
102
|
-
setDisplayMessages((prev) => [...prev,
|
|
118
|
+
setDisplayMessages((prev) => [...prev, toDisplayMessage("assistant", final)]);
|
|
103
119
|
}
|
|
104
120
|
streamingRef.current = "";
|
|
105
121
|
setStreamingText("");
|
|
@@ -108,15 +124,20 @@ export function App({ session, workdir, onQuit }) {
|
|
|
108
124
|
case "error":
|
|
109
125
|
setDisplayMessages((prev) => [
|
|
110
126
|
...prev,
|
|
111
|
-
|
|
127
|
+
toDisplayMessage("assistant", `Error: ${event.message}`),
|
|
112
128
|
]);
|
|
113
129
|
streamingRef.current = "";
|
|
114
130
|
setStreamingText("");
|
|
115
131
|
setRunning(false);
|
|
132
|
+
setPendingCommand(null);
|
|
116
133
|
if (/Missing .*API_KEY/i.test(event.message)) {
|
|
117
134
|
openApiKeyPrompt(model, "none");
|
|
118
135
|
}
|
|
119
136
|
break;
|
|
137
|
+
case "permission_request":
|
|
138
|
+
setPendingCommand(event.request);
|
|
139
|
+
setOverlay("commandApproval");
|
|
140
|
+
break;
|
|
120
141
|
case "model_changed":
|
|
121
142
|
setModel(event.model);
|
|
122
143
|
break;
|
|
@@ -126,12 +147,24 @@ export function App({ session, workdir, onQuit }) {
|
|
|
126
147
|
return () => {
|
|
127
148
|
session.off("event", handler);
|
|
128
149
|
};
|
|
129
|
-
}, [session, model, openApiKeyPrompt]);
|
|
130
|
-
|
|
150
|
+
}, [session, model, openApiKeyPrompt, stdout]);
|
|
151
|
+
autoFollowRef.current = autoFollow;
|
|
152
|
+
useInput((input, key) => {
|
|
131
153
|
if (overlay !== "none")
|
|
132
154
|
return;
|
|
133
155
|
if (key.escape && running) {
|
|
134
156
|
session.abort();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (key.pageUp || (key.upArrow && key.shift)) {
|
|
160
|
+
autoFollowRef.current = false;
|
|
161
|
+
setAutoFollow(false);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (input === "g" && key.ctrl) {
|
|
165
|
+
autoFollowRef.current = true;
|
|
166
|
+
setAutoFollow(true);
|
|
167
|
+
scrollViewportToBottom(stdout);
|
|
135
168
|
}
|
|
136
169
|
}, { isActive: overlay === "none" });
|
|
137
170
|
const handleSubmit = useCallback(async (value) => {
|
|
@@ -144,6 +177,7 @@ export function App({ session, workdir, onQuit }) {
|
|
|
144
177
|
session.newSession();
|
|
145
178
|
setDisplayMessages([]);
|
|
146
179
|
setStreamingText("");
|
|
180
|
+
setAutoFollow(true);
|
|
147
181
|
return;
|
|
148
182
|
}
|
|
149
183
|
if (value === "/settings") {
|
|
@@ -165,7 +199,10 @@ export function App({ session, workdir, onQuit }) {
|
|
|
165
199
|
await session.prompt(value);
|
|
166
200
|
}, [session, running, onQuit, exit, model, settings, openApiKeyPrompt]);
|
|
167
201
|
const hasChat = displayMessages.length > 0 || streamingText.length > 0;
|
|
168
|
-
return (_jsxs(Box, { flexDirection: "column",
|
|
202
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { paddingX: 2, marginBottom: 1, flexShrink: 0, children: _jsx(StartupBanner, { theme: theme, compact: hasChat }) }), _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: () => {
|
|
203
|
+
autoFollowRef.current = false;
|
|
204
|
+
setAutoFollow(false);
|
|
205
|
+
} }) })), overlay === "model" && (_jsx(ModelSelector, { theme: theme, settings: settings, filter: modelFilter, onSelect: (m) => {
|
|
169
206
|
if (!hasProviderAuth(m.provider, settings)) {
|
|
170
207
|
openApiKeyPrompt(m, "model");
|
|
171
208
|
return;
|
|
@@ -186,5 +223,13 @@ export function App({ session, workdir, onQuit }) {
|
|
|
186
223
|
setSettings(s);
|
|
187
224
|
}, onSetApiKey: (provider) => {
|
|
188
225
|
openApiKeyPrompt(modelForProvider(provider, settings), "settings");
|
|
189
|
-
}, onClose: () => setOverlay("none") }))
|
|
226
|
+
}, onClose: () => setOverlay("none") })), overlay === "commandApproval" && pendingCommand && (_jsx(CommandApprovalPrompt, { theme: theme, request: pendingCommand, onApprove: () => {
|
|
227
|
+
session.respondToPermission(true);
|
|
228
|
+
setPendingCommand(null);
|
|
229
|
+
setOverlay("none");
|
|
230
|
+
}, onDeny: () => {
|
|
231
|
+
session.respondToPermission(false);
|
|
232
|
+
setPendingCommand(null);
|
|
233
|
+
setOverlay("none");
|
|
234
|
+
} }))] }));
|
|
190
235
|
}
|
package/dist/ui/ChatView.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/ui/ChatView.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
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
6
|
import { Panel } from "./Panel.js";
|
|
@@ -8,7 +8,16 @@ import { modelRef } from "../config/models.js";
|
|
|
8
8
|
function truncate(text, max) {
|
|
9
9
|
return text.length > max ? text.slice(0, max) + "…" : text;
|
|
10
10
|
}
|
|
11
|
-
|
|
11
|
+
function StaticMessage({ msg, theme, model, }) {
|
|
12
|
+
if (msg.role === "user") {
|
|
13
|
+
return (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: _jsx(LeftBorder, { theme: theme, borderColor: theme.primary, marginBottom: 0, children: _jsx(Text, { color: theme.text, children: msg.content }) }) }));
|
|
14
|
+
}
|
|
15
|
+
if (msg.role === "assistant") {
|
|
16
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, 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)] })] }));
|
|
17
|
+
}
|
|
18
|
+
return (_jsx(Box, { paddingLeft: 1, marginBottom: 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, 500)] })] }) }));
|
|
19
|
+
}
|
|
20
|
+
export function ChatView({ messages, theme, model, streamingText, running, autoFollow = true, }) {
|
|
12
21
|
const [spinIdx, setSpinIdx] = useState(0);
|
|
13
22
|
const hasContent = messages.length > 0 || (streamingText?.length ?? 0) > 0;
|
|
14
23
|
useEffect(() => {
|
|
@@ -20,5 +29,6 @@ export function ChatView({ messages, theme, model, streamingText, running }) {
|
|
|
20
29
|
if (!hasContent) {
|
|
21
30
|
return null;
|
|
22
31
|
}
|
|
23
|
-
|
|
32
|
+
const showWorking = running && !streamingText && messages.length > 0;
|
|
33
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Static, { items: messages, children: (msg) => (_jsx(StaticMessage, { msg: msg, theme: theme, model: model }, msg.id)) }), (streamingText || showWorking) && (_jsxs(Panel, { theme: theme, borderColor: theme.border, marginBottom: 1, children: [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", !autoFollow && (_jsx(Text, { color: theme.warning, children: " \u00B7 paused (Ctrl+G to follow)" }))] })] })), showWorking && (_jsx(Box, { paddingLeft: 1, children: _jsxs(Text, { color: theme.textMuted, children: [_jsx(Text, { color: theme.primary, children: SPINNER_FRAMES[spinIdx] }), " working\u2026"] }) }))] }))] }));
|
|
24
34
|
}
|
|
@@ -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
|
+
}
|
package/dist/ui/Editor.d.ts
CHANGED
|
@@ -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,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
|
+
}
|
package/dist/ui/theme.js
CHANGED