@agent-api/cli 0.0.1
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/LICENSE +21 -0
- package/README.md +231 -0
- package/dist/agent/runner.d.ts +115 -0
- package/dist/agent/runner.js +483 -0
- package/dist/agent.d.ts +2 -0
- package/dist/agent.js +2 -0
- package/dist/chat-options.d.ts +37 -0
- package/dist/chat-options.js +42 -0
- package/dist/config.d.ts +44 -0
- package/dist/config.js +111 -0
- package/dist/conversation/index.d.ts +17 -0
- package/dist/conversation/index.js +54 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +308 -0
- package/dist/profile.d.ts +57 -0
- package/dist/profile.js +211 -0
- package/dist/runtime/index.d.ts +5 -0
- package/dist/runtime/index.js +12 -0
- package/dist/tui/chat.d.ts +5 -0
- package/dist/tui/chat.js +1276 -0
- package/dist/tui/workbench.d.ts +154 -0
- package/dist/tui/workbench.js +288 -0
- package/dist/update.d.ts +16 -0
- package/dist/update.js +74 -0
- package/dist/workdir/index.d.ts +22 -0
- package/dist/workdir/index.js +46 -0
- package/package.json +61 -0
package/dist/tui/chat.js
ADDED
|
@@ -0,0 +1,1276 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useReducer, useRef, useState } from "react";
|
|
3
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
4
|
+
import { defaultBaseURL, loadWorkbenchPreferences, updateWorkbenchPreferences, } from "../config.js";
|
|
5
|
+
import { conversationSummary, clearPresetToolCatalogCache, deleteConversation, isAvailablePreset, listAvailablePresets, listConversations, resumeAgentAfterLocalApproval, runAgentTurn, } from "../agent.js";
|
|
6
|
+
import { openWorkdir, } from "../workdir/index.js";
|
|
7
|
+
import { createLocalShellToolRegistry, createLocalWorkdirToolRegistry } from "@agent-api/sdk/local";
|
|
8
|
+
import { activityColor, createInitialWorkbenchState, formatBytes, helpText, parsePendingApprovalCommand, parseWorkbenchCommand, workbenchReducer, workdirText, } from "./workbench.js";
|
|
9
|
+
import { deleteProfile, formatDeviceUserCode, getAuthStatus, loginWithAPIKey, openBrowserURL, refreshActiveProfileIfNeeded, resolveRuntimeProfile, saveBrowserProfile, startBrowserAuthChallenge, waitForBrowserAuthChallenge, } from "../profile.js";
|
|
10
|
+
import { checkForUpdate, formatUpdateNotice } from "../update.js";
|
|
11
|
+
export function ChatApp({ options }) {
|
|
12
|
+
return _jsx(AuthenticatedChatApp, { options: options });
|
|
13
|
+
}
|
|
14
|
+
const authMethods = [
|
|
15
|
+
{ method: "browser", label: "Browser session", description: "Interactive login with refreshable local session." },
|
|
16
|
+
{ method: "api_key", label: "API key", description: "Paste a static API key for shell-only environments." },
|
|
17
|
+
{ method: "exit", label: "Exit", description: "Leave without signing in." },
|
|
18
|
+
];
|
|
19
|
+
function AuthenticatedChatApp({ options }) {
|
|
20
|
+
const app = useApp();
|
|
21
|
+
const busyRef = useRef(false);
|
|
22
|
+
const [currentProfile, setCurrentProfile] = useState(options.profile || "default");
|
|
23
|
+
const [auth, setAuth] = useState({
|
|
24
|
+
status: "checking",
|
|
25
|
+
selectedMethod: 0,
|
|
26
|
+
profile: options.profile || "default",
|
|
27
|
+
baseURL: process.env.AGENT_API_BASE_URL || defaultBaseURL,
|
|
28
|
+
apiKey: process.env.AGENT_API_KEY || "",
|
|
29
|
+
message: "Checking local auth profile...",
|
|
30
|
+
error: "",
|
|
31
|
+
browserURL: "",
|
|
32
|
+
browserCode: "",
|
|
33
|
+
});
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
let mounted = true;
|
|
36
|
+
refreshActiveProfileIfNeeded(options.profile, 5 * 60_000)
|
|
37
|
+
.then((result) => {
|
|
38
|
+
if (!mounted)
|
|
39
|
+
return;
|
|
40
|
+
setCurrentProfile(result.profile.name);
|
|
41
|
+
setAuth((current) => ({ ...current, status: "ready", message: "Authenticated.", error: "" }));
|
|
42
|
+
})
|
|
43
|
+
.catch((error) => {
|
|
44
|
+
if (!mounted)
|
|
45
|
+
return;
|
|
46
|
+
setAuth((current) => ({
|
|
47
|
+
...current,
|
|
48
|
+
status: "select",
|
|
49
|
+
message: "Choose an auth method to continue into the workbench.",
|
|
50
|
+
error: userFacingError(error),
|
|
51
|
+
}));
|
|
52
|
+
});
|
|
53
|
+
return () => {
|
|
54
|
+
mounted = false;
|
|
55
|
+
};
|
|
56
|
+
}, [options.profile]);
|
|
57
|
+
useInput((input, key) => {
|
|
58
|
+
if (key.ctrl && input === "c") {
|
|
59
|
+
app.exit();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (auth.status === "ready" || auth.status === "checking" || auth.status === "browser_waiting")
|
|
63
|
+
return;
|
|
64
|
+
if (auth.status === "select") {
|
|
65
|
+
if (key.upArrow) {
|
|
66
|
+
setAuth((current) => ({ ...current, selectedMethod: Math.max(0, current.selectedMethod - 1) }));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (key.downArrow) {
|
|
70
|
+
setAuth((current) => ({ ...current, selectedMethod: Math.min(authMethods.length - 1, current.selectedMethod + 1) }));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (key.return) {
|
|
74
|
+
const method = authMethods[auth.selectedMethod]?.method;
|
|
75
|
+
if (method === "exit") {
|
|
76
|
+
app.exit();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
setAuth((current) => ({
|
|
80
|
+
...current,
|
|
81
|
+
status: method === "api_key" ? "api_profile" : "browser_profile",
|
|
82
|
+
message: method === "api_key" ? "Save an API key profile." : "Create a browser session profile.",
|
|
83
|
+
error: "",
|
|
84
|
+
}));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (key.return) {
|
|
90
|
+
void submitAuthField();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (key.backspace || key.delete) {
|
|
94
|
+
editAuthField((value) => value.slice(0, -1));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (input && !key.ctrl && !key.meta) {
|
|
98
|
+
editAuthField((value) => value + input);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
function editAuthField(update) {
|
|
102
|
+
setAuth((current) => {
|
|
103
|
+
switch (current.status) {
|
|
104
|
+
case "api_profile":
|
|
105
|
+
case "browser_profile":
|
|
106
|
+
return { ...current, profile: update(current.profile), error: "" };
|
|
107
|
+
case "api_base_url":
|
|
108
|
+
case "browser_base_url":
|
|
109
|
+
return { ...current, baseURL: update(current.baseURL), error: "" };
|
|
110
|
+
case "api_key":
|
|
111
|
+
return { ...current, apiKey: update(current.apiKey), error: "" };
|
|
112
|
+
default:
|
|
113
|
+
return current;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async function submitAuthField() {
|
|
118
|
+
if (busyRef.current)
|
|
119
|
+
return;
|
|
120
|
+
const profile = auth.profile.trim() || "default";
|
|
121
|
+
const baseURL = auth.baseURL.trim() || defaultBaseURL;
|
|
122
|
+
switch (auth.status) {
|
|
123
|
+
case "api_profile":
|
|
124
|
+
setAuth((current) => ({ ...current, profile, status: "api_base_url", error: "" }));
|
|
125
|
+
return;
|
|
126
|
+
case "api_base_url":
|
|
127
|
+
setAuth((current) => ({ ...current, baseURL, status: "api_key", error: "" }));
|
|
128
|
+
return;
|
|
129
|
+
case "api_key": {
|
|
130
|
+
const apiKey = auth.apiKey.trim();
|
|
131
|
+
if (!apiKey) {
|
|
132
|
+
setAuth((current) => ({ ...current, error: "API key is required." }));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
busyRef.current = true;
|
|
136
|
+
setAuth((current) => ({ ...current, message: "Saving API key profile...", error: "" }));
|
|
137
|
+
try {
|
|
138
|
+
const saved = await loginWithAPIKey({ profile, baseURL, apiKey });
|
|
139
|
+
setCurrentProfile(saved.name);
|
|
140
|
+
setAuth((current) => ({ ...current, status: "ready", message: `Signed in as profile "${profile}".`, error: "" }));
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
setAuth((current) => ({ ...current, error: userFacingError(error) }));
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
busyRef.current = false;
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
case "browser_profile":
|
|
151
|
+
setAuth((current) => ({ ...current, profile, status: "browser_base_url", error: "" }));
|
|
152
|
+
return;
|
|
153
|
+
case "browser_base_url":
|
|
154
|
+
await runBrowserLogin(profile, baseURL);
|
|
155
|
+
return;
|
|
156
|
+
default:
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async function runBrowserLogin(profile, baseURL) {
|
|
161
|
+
if (busyRef.current)
|
|
162
|
+
return;
|
|
163
|
+
busyRef.current = true;
|
|
164
|
+
setAuth((current) => ({
|
|
165
|
+
...current,
|
|
166
|
+
profile,
|
|
167
|
+
baseURL,
|
|
168
|
+
status: "browser_waiting",
|
|
169
|
+
message: "Starting browser auth challenge...",
|
|
170
|
+
error: "",
|
|
171
|
+
browserURL: "",
|
|
172
|
+
browserCode: "",
|
|
173
|
+
}));
|
|
174
|
+
try {
|
|
175
|
+
const challenge = await startBrowserAuthChallenge({ baseURL, clientName: "Agent API TUI" });
|
|
176
|
+
setAuth((current) => ({
|
|
177
|
+
...current,
|
|
178
|
+
message: "Open the URL to approve this terminal session.",
|
|
179
|
+
browserURL: challenge.verification_uri_complete,
|
|
180
|
+
browserCode: formatDeviceUserCode(challenge.user_code),
|
|
181
|
+
}));
|
|
182
|
+
await openBrowserURL(challenge.verification_uri_complete).catch(() => undefined);
|
|
183
|
+
const session = await waitForBrowserAuthChallenge({
|
|
184
|
+
baseURL,
|
|
185
|
+
challenge,
|
|
186
|
+
on_poll(result) {
|
|
187
|
+
if (result.status && result.status !== "pending") {
|
|
188
|
+
setAuth((current) => ({ ...current, message: `Browser auth status: ${result.status}` }));
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
const saved = await saveBrowserProfile(profile, baseURL, session);
|
|
193
|
+
setCurrentProfile(saved.name);
|
|
194
|
+
setAuth((current) => ({ ...current, status: "ready", message: `Signed in as profile "${profile}".`, error: "" }));
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
setAuth((current) => ({
|
|
198
|
+
...current,
|
|
199
|
+
status: "select",
|
|
200
|
+
message: "Browser auth did not complete. Choose an auth method to continue.",
|
|
201
|
+
error: userFacingError(error),
|
|
202
|
+
}));
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
busyRef.current = false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (auth.status === "ready") {
|
|
209
|
+
return (_jsx(WorkbenchApp, { onLogin: () => {
|
|
210
|
+
setAuth((current) => ({
|
|
211
|
+
...current,
|
|
212
|
+
profile: currentProfile,
|
|
213
|
+
status: "select",
|
|
214
|
+
message: "Choose an auth method to continue into the workbench.",
|
|
215
|
+
error: "",
|
|
216
|
+
}));
|
|
217
|
+
}, onLogout: () => {
|
|
218
|
+
setAuth((current) => ({
|
|
219
|
+
...current,
|
|
220
|
+
profile: currentProfile,
|
|
221
|
+
status: "select",
|
|
222
|
+
message: `Logged out of profile "${currentProfile}" for this app session. Choose an auth method to continue.`,
|
|
223
|
+
error: "",
|
|
224
|
+
}));
|
|
225
|
+
}, onDeleteProfile: async () => {
|
|
226
|
+
await deleteProfile(currentProfile);
|
|
227
|
+
setAuth((current) => ({
|
|
228
|
+
...current,
|
|
229
|
+
status: "select",
|
|
230
|
+
message: `Deleted profile "${currentProfile}". Choose an auth method to continue.`,
|
|
231
|
+
error: "",
|
|
232
|
+
}));
|
|
233
|
+
}, onSwitchProfile: (name) => {
|
|
234
|
+
setAuth((current) => ({
|
|
235
|
+
...current,
|
|
236
|
+
profile: name || currentProfile,
|
|
237
|
+
status: "select",
|
|
238
|
+
message: name ? `Choose an auth method for profile "${name}".` : "Choose an auth method for another profile.",
|
|
239
|
+
error: "",
|
|
240
|
+
}));
|
|
241
|
+
}, options: { ...options, profile: currentProfile }, profileName: currentProfile }));
|
|
242
|
+
}
|
|
243
|
+
return _jsx(AuthGate, { state: auth });
|
|
244
|
+
}
|
|
245
|
+
function WorkbenchApp({ onLogin, onLogout, onDeleteProfile, onSwitchProfile, options, profileName, }) {
|
|
246
|
+
const app = useApp();
|
|
247
|
+
const workdirRef = useRef(null);
|
|
248
|
+
const initialPromptSubmittedRef = useRef(false);
|
|
249
|
+
const pendingApprovalInvalidInputsRef = useRef(0);
|
|
250
|
+
const authRefreshWarningShownRef = useRef(false);
|
|
251
|
+
const textDeltaBufferRef = useRef(null);
|
|
252
|
+
const textDeltaFlushTimerRef = useRef(null);
|
|
253
|
+
const activeAbortControllerRef = useRef(null);
|
|
254
|
+
const activeResponseIDRef = useRef(null);
|
|
255
|
+
const cancelledResponseIDsRef = useRef(new Set());
|
|
256
|
+
const updateNoticeShownRef = useRef(false);
|
|
257
|
+
const [draft, setDraft] = useState("");
|
|
258
|
+
const [spinnerFrame, setSpinnerFrame] = useState(0);
|
|
259
|
+
const [runPreset, setRunPreset] = useState(options.preset);
|
|
260
|
+
const [runModel, setRunModel] = useState(options.model);
|
|
261
|
+
const [workbenchPreferences, setWorkbenchPreferences] = useState({});
|
|
262
|
+
const [state, dispatch] = useReducer(workbenchReducer, {
|
|
263
|
+
accessMode: options.accessMode,
|
|
264
|
+
conversation: options.conversation,
|
|
265
|
+
contextEnabled: Boolean(options.includeLocalContext || options.workdir),
|
|
266
|
+
}, createInitialWorkbenchState);
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
let mounted = true;
|
|
269
|
+
if (!updateNoticeShownRef.current && process.env.AGENT_TUI_UPDATE_CHECK !== "0") {
|
|
270
|
+
updateNoticeShownRef.current = true;
|
|
271
|
+
checkForUpdate()
|
|
272
|
+
.then((result) => {
|
|
273
|
+
if (!mounted || !result?.updateAvailable)
|
|
274
|
+
return;
|
|
275
|
+
const notice = formatUpdateNotice(result);
|
|
276
|
+
dispatch({ type: "activity.add", level: "warning", text: `Update available: ${result.latest}` });
|
|
277
|
+
dispatch({ type: "message.add", role: "system", text: notice });
|
|
278
|
+
})
|
|
279
|
+
.catch(() => undefined);
|
|
280
|
+
}
|
|
281
|
+
loadWorkbenchPreferences()
|
|
282
|
+
.then((preferences) => {
|
|
283
|
+
if (!mounted)
|
|
284
|
+
return;
|
|
285
|
+
setWorkbenchPreferences(preferences);
|
|
286
|
+
if (!options.presetExplicit && !options.modelExplicit) {
|
|
287
|
+
setRunPreset(effectiveDefaultPreset(preferences, options.preset));
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
.catch((error) => {
|
|
291
|
+
if (!mounted)
|
|
292
|
+
return;
|
|
293
|
+
dispatch({ type: "activity.add", level: "warning", text: `Config preferences unavailable: ${userFacingError(error)}` });
|
|
294
|
+
});
|
|
295
|
+
return () => {
|
|
296
|
+
mounted = false;
|
|
297
|
+
};
|
|
298
|
+
}, [options.modelExplicit, options.presetExplicit]);
|
|
299
|
+
useEffect(() => {
|
|
300
|
+
let mounted = true;
|
|
301
|
+
dispatch({ type: "activity.add", text: "Loading workdir" });
|
|
302
|
+
openWorkdir({ path: options.workdir || process.cwd() })
|
|
303
|
+
.then(async (workdir) => {
|
|
304
|
+
const summary = await workdir.summarize();
|
|
305
|
+
if (!mounted)
|
|
306
|
+
return;
|
|
307
|
+
workdirRef.current = workdir;
|
|
308
|
+
dispatch({
|
|
309
|
+
type: "workdir.set",
|
|
310
|
+
workdir: {
|
|
311
|
+
root: workdir.root,
|
|
312
|
+
name: workdir.name,
|
|
313
|
+
fileCount: summary.file_count,
|
|
314
|
+
totalBytes: summary.total_bytes,
|
|
315
|
+
scanTruncated: summary.scan_truncated,
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
})
|
|
319
|
+
.catch((error) => {
|
|
320
|
+
if (!mounted)
|
|
321
|
+
return;
|
|
322
|
+
dispatch({
|
|
323
|
+
type: "activity.add",
|
|
324
|
+
level: "error",
|
|
325
|
+
text: `Workdir unavailable: ${error instanceof Error ? error.message : String(error)}`,
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
return () => {
|
|
329
|
+
mounted = false;
|
|
330
|
+
};
|
|
331
|
+
}, [options.workdir]);
|
|
332
|
+
useEffect(() => {
|
|
333
|
+
let mounted = true;
|
|
334
|
+
const refreshWindowMs = 5 * 60_000;
|
|
335
|
+
const refreshIntervalMs = 60_000;
|
|
336
|
+
const refreshAuth = async () => {
|
|
337
|
+
try {
|
|
338
|
+
const result = await refreshActiveProfileIfNeeded(options.profile, refreshWindowMs);
|
|
339
|
+
if (!mounted)
|
|
340
|
+
return;
|
|
341
|
+
if (result.refreshed) {
|
|
342
|
+
authRefreshWarningShownRef.current = false;
|
|
343
|
+
dispatch({ type: "activity.add", level: "success", text: "Auth session refreshed" });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
if (!mounted || authRefreshWarningShownRef.current)
|
|
348
|
+
return;
|
|
349
|
+
authRefreshWarningShownRef.current = true;
|
|
350
|
+
dispatch({
|
|
351
|
+
type: "message.add",
|
|
352
|
+
role: "system",
|
|
353
|
+
text: `${userFacingError(error)}\n\nClosing the workbench because authenticated agent conversations are unavailable.`,
|
|
354
|
+
});
|
|
355
|
+
dispatch({ type: "activity.add", level: "error", text: "Auth session needs login; closing" });
|
|
356
|
+
setTimeout(() => {
|
|
357
|
+
if (mounted)
|
|
358
|
+
app.exit();
|
|
359
|
+
}, 1500);
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
void refreshAuth();
|
|
363
|
+
const interval = setInterval(refreshAuth, refreshIntervalMs);
|
|
364
|
+
return () => {
|
|
365
|
+
mounted = false;
|
|
366
|
+
clearInterval(interval);
|
|
367
|
+
};
|
|
368
|
+
}, [app, options.profile]);
|
|
369
|
+
useInput((input, key) => {
|
|
370
|
+
if (key.ctrl && input === "c") {
|
|
371
|
+
app.exit();
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (state.busy) {
|
|
375
|
+
if (key.escape) {
|
|
376
|
+
void abortActiveTurn("Abort requested.");
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (key.return) {
|
|
380
|
+
const command = draft.trim();
|
|
381
|
+
setDraft("");
|
|
382
|
+
if (command === "/abort" || command === "/cancel") {
|
|
383
|
+
void abortActiveTurn("Abort requested.");
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (command) {
|
|
387
|
+
dispatch({ type: "message.add", role: "system", text: "Agent turn is running. Use /abort or Esc to cancel it." });
|
|
388
|
+
dispatch({ type: "activity.add", level: "warning", text: "Input ignored while agent is running" });
|
|
389
|
+
}
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (key.backspace || key.delete) {
|
|
393
|
+
setDraft((current) => current.slice(0, -1));
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (input && !key.ctrl && !key.meta) {
|
|
397
|
+
setDraft((current) => current + input);
|
|
398
|
+
}
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (key.return) {
|
|
402
|
+
const prompt = draft.trim();
|
|
403
|
+
if (!prompt)
|
|
404
|
+
return;
|
|
405
|
+
setDraft("");
|
|
406
|
+
void submit(prompt);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (key.backspace || key.delete) {
|
|
410
|
+
setDraft((current) => current.slice(0, -1));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (input && !key.ctrl && !key.meta) {
|
|
414
|
+
setDraft((current) => current + input);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
useEffect(() => {
|
|
418
|
+
if (initialPromptSubmittedRef.current || state.busy)
|
|
419
|
+
return;
|
|
420
|
+
const initialPrompt = options.promptParts.join(" ").trim();
|
|
421
|
+
if (!initialPrompt)
|
|
422
|
+
return;
|
|
423
|
+
if (!workdirRef.current)
|
|
424
|
+
return;
|
|
425
|
+
initialPromptSubmittedRef.current = true;
|
|
426
|
+
void send(initialPrompt);
|
|
427
|
+
}, [options.promptParts, state.busy, state.workdir]);
|
|
428
|
+
useEffect(() => {
|
|
429
|
+
pendingApprovalInvalidInputsRef.current = 0;
|
|
430
|
+
}, [state.pendingLocalTool?.id]);
|
|
431
|
+
useEffect(() => {
|
|
432
|
+
if (!state.busy) {
|
|
433
|
+
setSpinnerFrame(0);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const interval = setInterval(() => {
|
|
437
|
+
setSpinnerFrame((frame) => frame + 1);
|
|
438
|
+
}, 120);
|
|
439
|
+
return () => clearInterval(interval);
|
|
440
|
+
}, [state.busy]);
|
|
441
|
+
useEffect(() => {
|
|
442
|
+
return () => {
|
|
443
|
+
if (textDeltaFlushTimerRef.current) {
|
|
444
|
+
clearTimeout(textDeltaFlushTimerRef.current);
|
|
445
|
+
textDeltaFlushTimerRef.current = null;
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
}, []);
|
|
449
|
+
async function submit(input) {
|
|
450
|
+
if (state.pendingLocalTool) {
|
|
451
|
+
const pendingApprovalCommand = parsePendingApprovalCommand(input);
|
|
452
|
+
if (pendingApprovalCommand) {
|
|
453
|
+
pendingApprovalInvalidInputsRef.current = 0;
|
|
454
|
+
await runCommand(pendingApprovalCommand);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
handleInvalidPendingApprovalInput();
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const command = parseWorkbenchCommand(input);
|
|
461
|
+
if (command) {
|
|
462
|
+
await runCommand(command);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
await send(input);
|
|
466
|
+
}
|
|
467
|
+
function handleInvalidPendingApprovalInput() {
|
|
468
|
+
pendingApprovalInvalidInputsRef.current += 1;
|
|
469
|
+
const attempts = pendingApprovalInvalidInputsRef.current;
|
|
470
|
+
const maxAttempts = 3;
|
|
471
|
+
if (attempts >= maxAttempts) {
|
|
472
|
+
dispatch({
|
|
473
|
+
type: "message.add",
|
|
474
|
+
role: "system",
|
|
475
|
+
text: "Local approval aborted after too many invalid inputs. The pending action was not executed.",
|
|
476
|
+
});
|
|
477
|
+
dispatch({ type: "activity.add", level: "warning", text: "Local approval aborted" });
|
|
478
|
+
dispatch({ type: "local_tool.pending.clear" });
|
|
479
|
+
pendingApprovalInvalidInputsRef.current = 0;
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
dispatch({
|
|
483
|
+
type: "message.add",
|
|
484
|
+
role: "system",
|
|
485
|
+
text: `Local approval is pending. Enter /apply or /yes to execute once, /apply-all or /yes-all to allow future local actions, or /reject or /no to discard. Invalid input ${attempts}/${maxAttempts}.`,
|
|
486
|
+
});
|
|
487
|
+
dispatch({ type: "activity.add", level: "warning", text: "Waiting for local approval command" });
|
|
488
|
+
}
|
|
489
|
+
async function runCommand(command) {
|
|
490
|
+
switch (command.kind) {
|
|
491
|
+
case "invalid":
|
|
492
|
+
dispatch({
|
|
493
|
+
type: "message.add",
|
|
494
|
+
role: "system",
|
|
495
|
+
text: `Unknown command: /${command.command}\nType /help for supported commands.`,
|
|
496
|
+
});
|
|
497
|
+
dispatch({ type: "activity.add", level: "warning", text: `Unknown command: /${command.command}` });
|
|
498
|
+
return;
|
|
499
|
+
case "abort":
|
|
500
|
+
if (!state.busy) {
|
|
501
|
+
dispatch({ type: "message.add", role: "system", text: "No agent turn is running." });
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
await abortActiveTurn("Abort requested.");
|
|
505
|
+
return;
|
|
506
|
+
case "quit":
|
|
507
|
+
app.exit();
|
|
508
|
+
return;
|
|
509
|
+
case "help":
|
|
510
|
+
dispatch({ type: "message.add", role: "system", text: helpText() });
|
|
511
|
+
return;
|
|
512
|
+
case "clear":
|
|
513
|
+
dispatch({ type: "messages.clear" });
|
|
514
|
+
return;
|
|
515
|
+
case "login":
|
|
516
|
+
onLogin();
|
|
517
|
+
return;
|
|
518
|
+
case "logout":
|
|
519
|
+
dispatch({ type: "activity.add", text: `Logged out: ${profileName}` });
|
|
520
|
+
onLogout();
|
|
521
|
+
return;
|
|
522
|
+
case "delete_profile":
|
|
523
|
+
dispatch({ type: "activity.add", level: "warning", text: `Deleting profile: ${profileName}` });
|
|
524
|
+
await onDeleteProfile();
|
|
525
|
+
return;
|
|
526
|
+
case "switch_profile":
|
|
527
|
+
onSwitchProfile(command.name);
|
|
528
|
+
return;
|
|
529
|
+
case "auth_status":
|
|
530
|
+
await showAuthStatus();
|
|
531
|
+
return;
|
|
532
|
+
case "config":
|
|
533
|
+
await runConfigCommand(command);
|
|
534
|
+
return;
|
|
535
|
+
case "context":
|
|
536
|
+
dispatch({ type: "context.set", enabled: command.enabled ?? !state.contextEnabled });
|
|
537
|
+
return;
|
|
538
|
+
case "access":
|
|
539
|
+
if (!command.mode) {
|
|
540
|
+
dispatch({ type: "message.add", role: "system", text: `Local access: ${state.accessMode}. Use /access off, /access approval, or /access full.` });
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
dispatch({ type: "access.set", mode: command.mode });
|
|
544
|
+
return;
|
|
545
|
+
case "preset":
|
|
546
|
+
await runPresetCommand(command.value);
|
|
547
|
+
return;
|
|
548
|
+
case "model":
|
|
549
|
+
if (!command.value) {
|
|
550
|
+
dispatch({ type: "message.add", role: "system", text: `Model: ${runModel || "auto"}. Use /model <name> or /model auto.` });
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
setRunModel(normalizeOptionalSetting(command.value, ["auto", "none", "off", "clear"]));
|
|
554
|
+
dispatch({ type: "activity.add", text: `Model: ${normalizeOptionalSetting(command.value, ["auto", "none", "off", "clear"]) || "auto"}` });
|
|
555
|
+
return;
|
|
556
|
+
case "workdir":
|
|
557
|
+
if (command.enabled === undefined) {
|
|
558
|
+
dispatch({
|
|
559
|
+
type: "message.add",
|
|
560
|
+
role: "system",
|
|
561
|
+
text: [
|
|
562
|
+
workdirText(state.workdir),
|
|
563
|
+
"",
|
|
564
|
+
`local_workdir tool: ${state.contextEnabled ? "on" : "off"}`,
|
|
565
|
+
`local_shell tool: ${state.contextEnabled ? "on" : "off"}`,
|
|
566
|
+
"Use /access approval or /access full to expose local tools, or /access off to hide them.",
|
|
567
|
+
].join("\n"),
|
|
568
|
+
});
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
dispatch({ type: "context.set", enabled: command.enabled });
|
|
572
|
+
dispatch({
|
|
573
|
+
type: "activity.add",
|
|
574
|
+
level: command.enabled ? "success" : "warning",
|
|
575
|
+
text: `local tools ${command.enabled ? "enabled" : "disabled"}`,
|
|
576
|
+
});
|
|
577
|
+
dispatch({
|
|
578
|
+
type: "message.add",
|
|
579
|
+
role: "system",
|
|
580
|
+
text: command.enabled
|
|
581
|
+
? "local_workdir and local_shell are now available to the model in approval mode. Use /access full to allow execution without prompts."
|
|
582
|
+
: "local tools are now hidden from the model.",
|
|
583
|
+
});
|
|
584
|
+
return;
|
|
585
|
+
case "summary":
|
|
586
|
+
await showSummary();
|
|
587
|
+
return;
|
|
588
|
+
case "search":
|
|
589
|
+
await searchWorkdir(command.query);
|
|
590
|
+
return;
|
|
591
|
+
case "new_conversation":
|
|
592
|
+
await startNewConversation(command.name);
|
|
593
|
+
return;
|
|
594
|
+
case "switch_conversation":
|
|
595
|
+
switchConversation(command.name);
|
|
596
|
+
return;
|
|
597
|
+
case "list_conversations":
|
|
598
|
+
await showConversations();
|
|
599
|
+
return;
|
|
600
|
+
case "refresh_catalog":
|
|
601
|
+
clearPresetToolCatalogCache();
|
|
602
|
+
dispatch({ type: "activity.add", level: "success", text: "Preset and tool catalogs refreshed" });
|
|
603
|
+
dispatch({ type: "message.add", role: "system", text: "Cleared cached preset and server tool catalogs. The next agent turn will fetch fresh platform configuration." });
|
|
604
|
+
return;
|
|
605
|
+
case "preview":
|
|
606
|
+
showEditPreview();
|
|
607
|
+
return;
|
|
608
|
+
case "apply":
|
|
609
|
+
await applyPendingEdit(false);
|
|
610
|
+
return;
|
|
611
|
+
case "apply_all":
|
|
612
|
+
await applyPendingEdit(true);
|
|
613
|
+
return;
|
|
614
|
+
case "reject":
|
|
615
|
+
rejectPendingEdit();
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
async function runConfigCommand(command) {
|
|
620
|
+
if (!command.field) {
|
|
621
|
+
dispatch({
|
|
622
|
+
type: "message.add",
|
|
623
|
+
role: "system",
|
|
624
|
+
text: runConfigText({
|
|
625
|
+
profileName,
|
|
626
|
+
runPreset,
|
|
627
|
+
runModel,
|
|
628
|
+
accessMode: state.accessMode,
|
|
629
|
+
contextEnabled: state.contextEnabled,
|
|
630
|
+
defaultPreset: workbenchPreferences.defaultPreset,
|
|
631
|
+
}),
|
|
632
|
+
});
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (command.field === "preset") {
|
|
636
|
+
if (!command.value) {
|
|
637
|
+
dispatch({
|
|
638
|
+
type: "message.add",
|
|
639
|
+
role: "system",
|
|
640
|
+
text: `Default preset: ${formatDefaultPreset(workbenchPreferences.defaultPreset)}. Use /config preset <name>, /config preset none, or /config preset reset.`,
|
|
641
|
+
});
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const normalized = normalizeDefaultPreset(command.value);
|
|
645
|
+
if (typeof normalized === "string" && !(await validatePresetName(normalized))) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const preferences = await updateWorkbenchPreferences({ defaultPreset: normalized });
|
|
649
|
+
setWorkbenchPreferences(preferences);
|
|
650
|
+
if (!options.presetExplicit && !options.modelExplicit) {
|
|
651
|
+
setRunPreset(effectiveDefaultPreset(preferences, options.preset));
|
|
652
|
+
}
|
|
653
|
+
dispatch({
|
|
654
|
+
type: "message.add",
|
|
655
|
+
role: "system",
|
|
656
|
+
text: `Saved default preset: ${formatDefaultPreset(preferences.defaultPreset)}.`,
|
|
657
|
+
});
|
|
658
|
+
dispatch({ type: "activity.add", level: "success", text: `Default preset saved: ${formatDefaultPreset(preferences.defaultPreset)}` });
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
async function runPresetCommand(value) {
|
|
662
|
+
if (!value) {
|
|
663
|
+
dispatch({
|
|
664
|
+
type: "message.add",
|
|
665
|
+
role: "system",
|
|
666
|
+
text: await presetListText(`Preset: ${runPreset || "none"}. Use /preset <name> or /preset none.`),
|
|
667
|
+
});
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
const normalized = normalizeOptionalSetting(value, ["none", "off", "clear"]);
|
|
671
|
+
if (normalized && !(await validatePresetName(normalized))) {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
setRunPreset(normalized);
|
|
675
|
+
dispatch({ type: "activity.add", text: `Preset: ${normalized || "none"}` });
|
|
676
|
+
}
|
|
677
|
+
async function validatePresetName(preset) {
|
|
678
|
+
try {
|
|
679
|
+
if (await isAvailablePreset(profileName, preset))
|
|
680
|
+
return true;
|
|
681
|
+
dispatch({
|
|
682
|
+
type: "message.add",
|
|
683
|
+
role: "system",
|
|
684
|
+
text: await presetListText(`Unknown preset: ${preset}`),
|
|
685
|
+
});
|
|
686
|
+
dispatch({ type: "activity.add", level: "warning", text: `Unknown preset: ${preset}` });
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
catch (error) {
|
|
690
|
+
dispatch({ type: "message.add", role: "system", text: `Could not validate preset "${preset}": ${userFacingError(error)}` });
|
|
691
|
+
dispatch({ type: "activity.add", level: "error", text: "Preset catalog unavailable" });
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
async function presetListText(prefix) {
|
|
696
|
+
try {
|
|
697
|
+
const presets = await listAvailablePresets(profileName);
|
|
698
|
+
return [
|
|
699
|
+
prefix,
|
|
700
|
+
"",
|
|
701
|
+
"Available presets:",
|
|
702
|
+
...formatPresetList(presets),
|
|
703
|
+
].join("\n");
|
|
704
|
+
}
|
|
705
|
+
catch (error) {
|
|
706
|
+
return [
|
|
707
|
+
prefix,
|
|
708
|
+
"",
|
|
709
|
+
`Available presets could not be loaded: ${userFacingError(error)}`,
|
|
710
|
+
].join("\n");
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
async function showAuthStatus() {
|
|
714
|
+
dispatch({ type: "activity.add", text: "Checking auth status" });
|
|
715
|
+
try {
|
|
716
|
+
const status = await getAuthStatus(profileName);
|
|
717
|
+
dispatch({ type: "message.add", role: "system", text: authStatusText(status) });
|
|
718
|
+
dispatch({ type: "activity.add", level: "success", text: "Auth status ready" });
|
|
719
|
+
}
|
|
720
|
+
catch (error) {
|
|
721
|
+
dispatch({ type: "message.add", role: "system", text: userFacingError(error) });
|
|
722
|
+
dispatch({ type: "activity.add", level: "error", text: "Auth status failed" });
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
async function showSummary() {
|
|
726
|
+
const workdir = workdirRef.current;
|
|
727
|
+
if (!workdir) {
|
|
728
|
+
dispatch({ type: "message.add", role: "system", text: "Workdir is still loading." });
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
dispatch({ type: "activity.add", text: "Summarizing workdir" });
|
|
732
|
+
try {
|
|
733
|
+
const summary = await workdir.summarize();
|
|
734
|
+
const previews = summary.text_previews
|
|
735
|
+
.slice(0, 5)
|
|
736
|
+
.map((preview) => `- ${preview.path} (${formatBytes(preview.size)})`)
|
|
737
|
+
.join("\n");
|
|
738
|
+
dispatch({
|
|
739
|
+
type: "message.add",
|
|
740
|
+
role: "system",
|
|
741
|
+
text: [
|
|
742
|
+
`Workdir summary for ${workdir.name}`,
|
|
743
|
+
`Files: ${summary.file_count}`,
|
|
744
|
+
`Size: ${formatBytes(summary.total_bytes)}`,
|
|
745
|
+
previews ? `Previews:\n${previews}` : "No text previews available.",
|
|
746
|
+
].join("\n"),
|
|
747
|
+
});
|
|
748
|
+
dispatch({ type: "activity.add", level: "success", text: "Workdir summary ready" });
|
|
749
|
+
}
|
|
750
|
+
catch (error) {
|
|
751
|
+
dispatch({
|
|
752
|
+
type: "activity.add",
|
|
753
|
+
level: "error",
|
|
754
|
+
text: userFacingError(error),
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
async function startNewConversation(name) {
|
|
759
|
+
const conversation = name || createConversationName();
|
|
760
|
+
await deleteConversation(conversation, options.profile);
|
|
761
|
+
dispatch({ type: "messages.clear" });
|
|
762
|
+
dispatch({ type: "conversation.set", name: conversation });
|
|
763
|
+
dispatch({
|
|
764
|
+
type: "message.add",
|
|
765
|
+
role: "system",
|
|
766
|
+
text: `Started fresh conversation "${conversation}".`,
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
function switchConversation(name) {
|
|
770
|
+
dispatch({ type: "messages.clear" });
|
|
771
|
+
dispatch({ type: "conversation.set", name });
|
|
772
|
+
dispatch({
|
|
773
|
+
type: "message.add",
|
|
774
|
+
role: "system",
|
|
775
|
+
text: `Switched to conversation "${name}". Future turns will continue this handle when history exists.`,
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
async function showConversations() {
|
|
779
|
+
try {
|
|
780
|
+
const conversations = await listConversations(options.profile);
|
|
781
|
+
dispatch({
|
|
782
|
+
type: "message.add",
|
|
783
|
+
role: "system",
|
|
784
|
+
text: conversations.length === 0
|
|
785
|
+
? "No saved conversations yet."
|
|
786
|
+
: conversations.map(conversationSummary).join("\n"),
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
catch (error) {
|
|
790
|
+
dispatch({ type: "message.add", role: "system", text: userFacingError(error) });
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
async function searchWorkdir(query) {
|
|
794
|
+
const workdir = workdirRef.current;
|
|
795
|
+
if (!query) {
|
|
796
|
+
dispatch({ type: "message.add", role: "system", text: "Usage: /search <query>" });
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
if (!workdir) {
|
|
800
|
+
dispatch({ type: "message.add", role: "system", text: "Workdir is still loading." });
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
dispatch({ type: "activity.add", text: `Searching workdir: ${query}` });
|
|
804
|
+
try {
|
|
805
|
+
const results = await workdir.workdir.grep({ pattern: query, limit: 12 });
|
|
806
|
+
const matches = results.matches
|
|
807
|
+
.map((match) => `${match.path}:${match.line_number}: ${match.line.trim()}`)
|
|
808
|
+
.join("\n");
|
|
809
|
+
dispatch({
|
|
810
|
+
type: "message.add",
|
|
811
|
+
role: "system",
|
|
812
|
+
text: matches || `No matches for "${query}".`,
|
|
813
|
+
});
|
|
814
|
+
dispatch({
|
|
815
|
+
type: "activity.add",
|
|
816
|
+
level: "success",
|
|
817
|
+
text: `Search complete: ${results.matches.length} matches`,
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
catch (error) {
|
|
821
|
+
dispatch({
|
|
822
|
+
type: "activity.add",
|
|
823
|
+
level: "error",
|
|
824
|
+
text: userFacingError(error),
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
function showEditPreview() {
|
|
829
|
+
if (state.pendingLocalTool) {
|
|
830
|
+
dispatch({ type: "message.add", role: "system", text: formatLocalToolApproval(state.pendingLocalTool) });
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
dispatch({ type: "message.add", role: "system", text: "No pending local action." });
|
|
834
|
+
}
|
|
835
|
+
async function applyPendingEdit(allowFutureLocalActions) {
|
|
836
|
+
const workdir = workdirRef.current;
|
|
837
|
+
if (!workdir) {
|
|
838
|
+
dispatch({ type: "message.add", role: "system", text: "Workdir is still loading." });
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
if (state.pendingLocalTool) {
|
|
842
|
+
dispatch({
|
|
843
|
+
type: "activity.add",
|
|
844
|
+
level: "warning",
|
|
845
|
+
text: `Applying local action: ${state.pendingLocalTool.name}${state.pendingLocalTool.action ? `.${state.pendingLocalTool.action}` : ""}`,
|
|
846
|
+
});
|
|
847
|
+
try {
|
|
848
|
+
const result = await applyLocalToolApproval(workdir, state.pendingLocalTool);
|
|
849
|
+
const nextAccessMode = allowFutureLocalActions ? "full" : state.accessMode;
|
|
850
|
+
if (allowFutureLocalActions) {
|
|
851
|
+
dispatch({ type: "access.set", mode: "full" });
|
|
852
|
+
}
|
|
853
|
+
dispatch({
|
|
854
|
+
type: "message.add",
|
|
855
|
+
role: "system",
|
|
856
|
+
text: [
|
|
857
|
+
allowFutureLocalActions
|
|
858
|
+
? "Applied local action. Future local actions in this workbench conversation are now allowed."
|
|
859
|
+
: "Applied local action once. Future local actions still require approval.",
|
|
860
|
+
"Continuing agent turn with the local result.",
|
|
861
|
+
"Result:",
|
|
862
|
+
JSON.stringify(result, null, 2),
|
|
863
|
+
].join("\n"),
|
|
864
|
+
});
|
|
865
|
+
dispatch({ type: "activity.add", level: "success", text: "Local action applied" });
|
|
866
|
+
const approval = state.pendingLocalTool;
|
|
867
|
+
dispatch({ type: "local_tool.pending.clear" });
|
|
868
|
+
const assistantId = `assistant-${Date.now()}`;
|
|
869
|
+
dispatch({ type: "busy.set", busy: true });
|
|
870
|
+
dispatch({ type: "message.add", role: "assistant", text: "", id: assistantId });
|
|
871
|
+
dispatch({ type: "assistant.active", id: assistantId });
|
|
872
|
+
dispatch({ type: "activity.add", text: "Continuing agent turn" });
|
|
873
|
+
const abortController = new AbortController();
|
|
874
|
+
activeAbortControllerRef.current = abortController;
|
|
875
|
+
activeResponseIDRef.current = null;
|
|
876
|
+
const continuation = await resumeAgentAfterLocalApproval({
|
|
877
|
+
...options,
|
|
878
|
+
preset: runPreset,
|
|
879
|
+
model: runModel,
|
|
880
|
+
stream: true,
|
|
881
|
+
file: undefined,
|
|
882
|
+
stdin: false,
|
|
883
|
+
conversation: state.currentConversation,
|
|
884
|
+
includeLocalContext: state.contextEnabled,
|
|
885
|
+
accessMode: nextAccessMode,
|
|
886
|
+
restartConversation: false,
|
|
887
|
+
abortSignal: abortController.signal,
|
|
888
|
+
}, approval, result, (event) => handleAgentEvent(event, assistantId));
|
|
889
|
+
dispatch({
|
|
890
|
+
type: "activity.add",
|
|
891
|
+
level: "success",
|
|
892
|
+
text: continuation.responseID ? `Agent turn continued: ${continuation.responseID}` : "Agent turn continued",
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
catch (error) {
|
|
896
|
+
handleTurnError(error);
|
|
897
|
+
}
|
|
898
|
+
finally {
|
|
899
|
+
flushTextDeltaBuffer();
|
|
900
|
+
activeAbortControllerRef.current = null;
|
|
901
|
+
activeResponseIDRef.current = null;
|
|
902
|
+
dispatch({ type: "busy.set", busy: false });
|
|
903
|
+
dispatch({ type: "assistant.active", id: null });
|
|
904
|
+
}
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
dispatch({ type: "message.add", role: "system", text: "No pending local action." });
|
|
908
|
+
}
|
|
909
|
+
function rejectPendingEdit() {
|
|
910
|
+
if (state.pendingLocalTool) {
|
|
911
|
+
dispatch({
|
|
912
|
+
type: "activity.add",
|
|
913
|
+
text: `Rejected local action: ${state.pendingLocalTool.name}${state.pendingLocalTool.action ? `.${state.pendingLocalTool.action}` : ""}`,
|
|
914
|
+
});
|
|
915
|
+
dispatch({ type: "local_tool.pending.clear" });
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
dispatch({ type: "message.add", role: "system", text: "No pending local action." });
|
|
919
|
+
}
|
|
920
|
+
async function send(prompt) {
|
|
921
|
+
const assistantId = `assistant-${Date.now()}`;
|
|
922
|
+
const abortController = new AbortController();
|
|
923
|
+
activeAbortControllerRef.current = abortController;
|
|
924
|
+
activeResponseIDRef.current = null;
|
|
925
|
+
dispatch({ type: "busy.set", busy: true });
|
|
926
|
+
dispatch({ type: "message.add", role: "user", text: prompt });
|
|
927
|
+
dispatch({ type: "message.add", role: "assistant", text: "", id: assistantId });
|
|
928
|
+
dispatch({ type: "assistant.active", id: assistantId });
|
|
929
|
+
dispatch({ type: "activity.add", text: "Agent turn started" });
|
|
930
|
+
try {
|
|
931
|
+
const result = await runAgentTurn({
|
|
932
|
+
...options,
|
|
933
|
+
preset: runPreset,
|
|
934
|
+
model: runModel,
|
|
935
|
+
promptParts: [prompt],
|
|
936
|
+
stream: true,
|
|
937
|
+
file: undefined,
|
|
938
|
+
stdin: false,
|
|
939
|
+
conversation: state.currentConversation,
|
|
940
|
+
includeLocalContext: state.contextEnabled,
|
|
941
|
+
accessMode: state.accessMode,
|
|
942
|
+
restartConversation: false,
|
|
943
|
+
abortSignal: abortController.signal,
|
|
944
|
+
}, (event) => handleAgentEvent(event, assistantId));
|
|
945
|
+
dispatch({
|
|
946
|
+
type: "activity.add",
|
|
947
|
+
level: "success",
|
|
948
|
+
text: result.responseID ? `Agent turn completed: ${result.responseID}` : "Agent turn completed",
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
catch (error) {
|
|
952
|
+
handleTurnError(error);
|
|
953
|
+
}
|
|
954
|
+
finally {
|
|
955
|
+
flushTextDeltaBuffer();
|
|
956
|
+
if (activeAbortControllerRef.current === abortController) {
|
|
957
|
+
activeAbortControllerRef.current = null;
|
|
958
|
+
}
|
|
959
|
+
activeResponseIDRef.current = null;
|
|
960
|
+
dispatch({ type: "busy.set", busy: false });
|
|
961
|
+
dispatch({ type: "assistant.active", id: null });
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
function handleAgentEvent(event, assistantId) {
|
|
965
|
+
switch (event.type) {
|
|
966
|
+
case "text.delta":
|
|
967
|
+
appendTextDeltaBuffered(assistantId, event.delta);
|
|
968
|
+
return;
|
|
969
|
+
case "response.started":
|
|
970
|
+
if (event.responseID)
|
|
971
|
+
activeResponseIDRef.current = event.responseID;
|
|
972
|
+
dispatch({ type: "activity.add", text: event.responseID ? `Response started: ${event.responseID}` : "Response started" });
|
|
973
|
+
return;
|
|
974
|
+
case "response.completed":
|
|
975
|
+
flushTextDeltaBuffer();
|
|
976
|
+
dispatch({ type: "activity.add", level: "success", text: event.responseID ? `Response completed: ${event.responseID}` : "Response completed" });
|
|
977
|
+
return;
|
|
978
|
+
case "response.failed":
|
|
979
|
+
flushTextDeltaBuffer();
|
|
980
|
+
dispatch({ type: "activity.add", level: "error", text: event.message });
|
|
981
|
+
return;
|
|
982
|
+
case "reasoning.started":
|
|
983
|
+
dispatch({ type: "activity.add", text: "Reasoning started" });
|
|
984
|
+
return;
|
|
985
|
+
case "reasoning.stopped":
|
|
986
|
+
dispatch({ type: "activity.add", text: event.thought ? `Reasoning stopped: ${event.thought}` : "Reasoning stopped" });
|
|
987
|
+
return;
|
|
988
|
+
case "reasoning.search_queries":
|
|
989
|
+
dispatch({ type: "activity.add", text: `Search queries: ${event.queries.join(", ") || "none"}` });
|
|
990
|
+
return;
|
|
991
|
+
case "reasoning.search_results":
|
|
992
|
+
dispatch({ type: "activity.add", text: `Search results: ${event.count}` });
|
|
993
|
+
return;
|
|
994
|
+
case "reasoning.fetch_url_queries":
|
|
995
|
+
dispatch({ type: "activity.add", text: `Fetch URLs: ${event.urls.join(", ") || "none"}` });
|
|
996
|
+
return;
|
|
997
|
+
case "reasoning.fetch_url_results":
|
|
998
|
+
dispatch({ type: "activity.add", text: `Fetched URL results: ${event.count}` });
|
|
999
|
+
return;
|
|
1000
|
+
case "tool.completed":
|
|
1001
|
+
dispatch({ type: "activity.add", level: event.status === "failed" ? "error" : "success", text: `Tool completed: ${event.name}${event.status ? ` (${event.status})` : ""}` });
|
|
1002
|
+
return;
|
|
1003
|
+
case "local_tool.completed":
|
|
1004
|
+
dispatch({
|
|
1005
|
+
type: "activity.add",
|
|
1006
|
+
level: event.requiresApproval ? "warning" : "success",
|
|
1007
|
+
text: `Local tool: ${event.name}${event.action ? `.${event.action}` : ""}${event.requiresApproval ? " (approval required)" : ""}`,
|
|
1008
|
+
});
|
|
1009
|
+
return;
|
|
1010
|
+
case "local_tool.approval_requested":
|
|
1011
|
+
dispatch({
|
|
1012
|
+
type: "local_tool.pending.set",
|
|
1013
|
+
approval: {
|
|
1014
|
+
name: event.name,
|
|
1015
|
+
action: event.action,
|
|
1016
|
+
arguments: event.arguments,
|
|
1017
|
+
preview: event.preview,
|
|
1018
|
+
callID: event.callID,
|
|
1019
|
+
responseID: event.responseID,
|
|
1020
|
+
},
|
|
1021
|
+
});
|
|
1022
|
+
dispatch({ type: "message.add", role: "system", text: formatLocalToolApproval(event) });
|
|
1023
|
+
return;
|
|
1024
|
+
case "model.requested":
|
|
1025
|
+
dispatch({ type: "activity.add", text: `Model requested: ${modelLabel(event.model, event.provider)}` });
|
|
1026
|
+
return;
|
|
1027
|
+
case "model.completed":
|
|
1028
|
+
dispatch({ type: "activity.add", level: "success", text: `Model completed: ${modelLabel(event.model, event.provider)}` });
|
|
1029
|
+
return;
|
|
1030
|
+
case "model.failed":
|
|
1031
|
+
dispatch({ type: "activity.add", level: "error", text: `Model failed: ${modelLabel(event.model, event.provider)}` });
|
|
1032
|
+
return;
|
|
1033
|
+
case "step.completed":
|
|
1034
|
+
dispatch({ type: "activity.add", level: "success", text: `Step completed: ${event.stepType || "step"}` });
|
|
1035
|
+
return;
|
|
1036
|
+
case "step.failed":
|
|
1037
|
+
dispatch({ type: "activity.add", level: "error", text: `Step failed: ${event.stepType || "step"}` });
|
|
1038
|
+
return;
|
|
1039
|
+
case "raw":
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
async function abortActiveTurn(reason) {
|
|
1044
|
+
const controller = activeAbortControllerRef.current;
|
|
1045
|
+
const responseID = activeResponseIDRef.current;
|
|
1046
|
+
if (!state.busy && !controller && !responseID) {
|
|
1047
|
+
dispatch({ type: "message.add", role: "system", text: "No agent turn is running." });
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
controller?.abort();
|
|
1051
|
+
dispatch({ type: "activity.add", level: "warning", text: reason });
|
|
1052
|
+
if (!responseID) {
|
|
1053
|
+
dispatch({ type: "message.add", role: "system", text: "Abort requested. No remote response ID is available yet." });
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
if (cancelledResponseIDsRef.current.has(responseID))
|
|
1057
|
+
return;
|
|
1058
|
+
cancelledResponseIDsRef.current.add(responseID);
|
|
1059
|
+
try {
|
|
1060
|
+
const runtimeProfile = await resolveRuntimeProfile(options.profile);
|
|
1061
|
+
const result = await runtimeProfile.client.responses.cancel(responseID);
|
|
1062
|
+
dispatch({
|
|
1063
|
+
type: "message.add",
|
|
1064
|
+
role: "system",
|
|
1065
|
+
text: result.interrupted
|
|
1066
|
+
? `Abort requested for response ${responseID}.`
|
|
1067
|
+
: `Abort requested locally. Remote response ${responseID} was not actively interrupted.`,
|
|
1068
|
+
});
|
|
1069
|
+
dispatch({
|
|
1070
|
+
type: "activity.add",
|
|
1071
|
+
level: result.interrupted ? "success" : "warning",
|
|
1072
|
+
text: result.interrupted ? `Response cancel requested: ${responseID}` : `Response was not active: ${responseID}`,
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
catch (error) {
|
|
1076
|
+
dispatch({ type: "message.add", role: "system", text: `Abort requested locally, but remote cancel failed: ${userFacingError(error)}` });
|
|
1077
|
+
dispatch({ type: "activity.add", level: "error", text: "Remote response cancel failed" });
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
function handleTurnError(error) {
|
|
1081
|
+
const message = userFacingError(error);
|
|
1082
|
+
const aborted = /aborted/i.test(message);
|
|
1083
|
+
dispatch({
|
|
1084
|
+
type: "message.add",
|
|
1085
|
+
role: "system",
|
|
1086
|
+
text: aborted ? "Agent turn aborted." : message,
|
|
1087
|
+
});
|
|
1088
|
+
dispatch({
|
|
1089
|
+
type: "activity.add",
|
|
1090
|
+
level: aborted ? "warning" : "error",
|
|
1091
|
+
text: aborted ? "Agent turn aborted" : message,
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
function appendTextDeltaBuffered(id, delta) {
|
|
1095
|
+
if (!delta)
|
|
1096
|
+
return;
|
|
1097
|
+
const current = textDeltaBufferRef.current;
|
|
1098
|
+
if (!current || current.id !== id) {
|
|
1099
|
+
flushTextDeltaBuffer();
|
|
1100
|
+
textDeltaBufferRef.current = { id, text: delta };
|
|
1101
|
+
}
|
|
1102
|
+
else {
|
|
1103
|
+
current.text += delta;
|
|
1104
|
+
}
|
|
1105
|
+
if (textDeltaFlushTimerRef.current)
|
|
1106
|
+
return;
|
|
1107
|
+
textDeltaFlushTimerRef.current = setTimeout(() => {
|
|
1108
|
+
textDeltaFlushTimerRef.current = null;
|
|
1109
|
+
flushTextDeltaBuffer();
|
|
1110
|
+
}, 80);
|
|
1111
|
+
}
|
|
1112
|
+
function flushTextDeltaBuffer() {
|
|
1113
|
+
if (textDeltaFlushTimerRef.current) {
|
|
1114
|
+
clearTimeout(textDeltaFlushTimerRef.current);
|
|
1115
|
+
textDeltaFlushTimerRef.current = null;
|
|
1116
|
+
}
|
|
1117
|
+
const buffered = textDeltaBufferRef.current;
|
|
1118
|
+
if (!buffered || !buffered.text)
|
|
1119
|
+
return;
|
|
1120
|
+
textDeltaBufferRef.current = null;
|
|
1121
|
+
dispatch({ type: "message.append", id: buffered.id, delta: buffered.text });
|
|
1122
|
+
}
|
|
1123
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { contextEnabled: state.contextEnabled, conversation: state.currentConversation, model: runModel || "auto", accessMode: state.accessMode, pendingLocalLabel: pendingLocalLabel(state), preset: runPreset || "none", profile: profileName, workdir: state.workdir?.root || options.workdir || process.cwd() }), _jsxs(Box, { marginTop: 1, children: [_jsx(Box, { flexDirection: "column", width: "72%", paddingRight: 1, children: state.messages.map((message) => (_jsx(MessageBlock, { active: message.id === state.activeAssistantMessageId, busy: state.busy, message: message, spinnerFrame: spinnerFrame }, message.id))) }), _jsxs(Box, { flexDirection: "column", width: "28%", borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Activity" }), state.activities.slice(-8).map((activity) => (_jsxs(Text, { color: activityColor(activity.level), children: [new Date(activity.timestamp).toLocaleTimeString(), " ", activity.text] }, activity.id)))] })] }), _jsxs(Box, { borderStyle: "single", borderColor: state.busy ? "yellow" : "green", paddingX: 1, children: [state.accessMode === "full" && (_jsx(Text, { color: "red", bold: true, inverse: true, children: "FULL ACCESS" })), state.accessMode === "full" && _jsx(Text, { children: " " }), _jsxs(Text, { color: state.busy ? "yellow" : "green", children: [state.busy ? "working" : "you", " "] }), state.busy ? (_jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: spinnerGlyph(spinnerFrame) }), " waiting for agent ", elapsedDots(spinnerFrame)] })) : (_jsxs(Text, { children: [draft, _jsx(Cursor, { visible: true })] }))] }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { color: "gray", children: "/help /auth /login /logout /switch-profile /delete-profile /config /preset /model /access /context /quit" }) })] }));
|
|
1124
|
+
}
|
|
1125
|
+
function AuthGate({ state }) {
|
|
1126
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Agent API Workbench" }), _jsx(Text, { color: "gray", children: "Authentication required before starting the conversation UI." })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: state.error ? "red" : "gray", children: state.error || state.message }), state.status === "checking" && _jsx(Text, { color: "yellow", children: "Checking..." }), state.status === "select" && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [authMethods.map((method, index) => (_jsxs(Text, { color: index === state.selectedMethod ? "green" : "gray", children: [index === state.selectedMethod ? "›" : " ", " ", method.label, " - ", method.description] }, method.method))), _jsx(Text, { color: "gray", children: "Use \u2191/\u2193 and Enter." })] })), state.status === "api_profile" && _jsx(AuthPrompt, { label: "Profile", value: state.profile }), state.status === "api_base_url" && _jsx(AuthPrompt, { label: "Base URL", value: state.baseURL }), state.status === "api_key" && _jsx(AuthPrompt, { label: "API key", value: state.apiKey ? "•".repeat(Math.min(state.apiKey.length, 32)) : "" }), state.status === "browser_profile" && _jsx(AuthPrompt, { label: "Profile", value: state.profile }), state.status === "browser_base_url" && _jsx(AuthPrompt, { label: "Base URL", value: state.baseURL }), state.status === "browser_waiting" && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [state.browserURL && _jsxs(Text, { children: ["URL: ", state.browserURL] }), state.browserCode && _jsxs(Text, { children: ["Code: ", state.browserCode] }), _jsx(Text, { color: "yellow", children: "Waiting for browser approval..." })] }))] })] }));
|
|
1127
|
+
}
|
|
1128
|
+
function AuthPrompt({ label, value }) {
|
|
1129
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: "green", paddingX: 1, marginTop: 1, children: [_jsxs(Text, { color: "green", children: [label, ": "] }), _jsx(Text, { children: value })] }));
|
|
1130
|
+
}
|
|
1131
|
+
function Header({ contextEnabled, conversation, accessMode, model, pendingLocalLabel, preset, profile, workdir, }) {
|
|
1132
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Agent API Workbench" }), _jsxs(Text, { color: "gray", children: ["profile=", profile, " conversation=", conversation, " preset=", preset, " model=", model] }), _jsxs(Text, { color: "gray", children: ["workdir=", workdir, " access=", accessMode, " local_tools=", contextEnabled ? "on" : "off", " pending=", pendingLocalLabel] })] }));
|
|
1133
|
+
}
|
|
1134
|
+
function MessageBlock({ active, busy, message, spinnerFrame, }) {
|
|
1135
|
+
const waiting = message.role === "assistant" && busy && active && !message.text;
|
|
1136
|
+
return (_jsxs(Box, { flexDirection: "column", marginBottom: message.role === "system" ? 0 : 1, children: [_jsx(Text, { color: roleColor(message.role), children: roleLabel(message.role) }), _jsx(Text, { children: message.text || (waiting ? `${spinnerGlyph(spinnerFrame)} thinking ${elapsedDots(spinnerFrame)}` : "") })] }));
|
|
1137
|
+
}
|
|
1138
|
+
function roleLabel(role) {
|
|
1139
|
+
if (role === "user")
|
|
1140
|
+
return "You";
|
|
1141
|
+
if (role === "assistant")
|
|
1142
|
+
return "Agent";
|
|
1143
|
+
return "System";
|
|
1144
|
+
}
|
|
1145
|
+
function roleColor(role) {
|
|
1146
|
+
if (role === "user")
|
|
1147
|
+
return "green";
|
|
1148
|
+
if (role === "assistant")
|
|
1149
|
+
return "cyan";
|
|
1150
|
+
return "gray";
|
|
1151
|
+
}
|
|
1152
|
+
function modelLabel(model, provider) {
|
|
1153
|
+
if (model && provider)
|
|
1154
|
+
return `${provider}/${model}`;
|
|
1155
|
+
return model || provider || "unknown";
|
|
1156
|
+
}
|
|
1157
|
+
function Cursor({ visible }) {
|
|
1158
|
+
return visible ? _jsx(Text, { inverse: true, children: " " }) : _jsx(Text, { children: " " });
|
|
1159
|
+
}
|
|
1160
|
+
function spinnerGlyph(frame) {
|
|
1161
|
+
return ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"][frame % 10];
|
|
1162
|
+
}
|
|
1163
|
+
function elapsedDots(frame) {
|
|
1164
|
+
return ".".repeat((Math.floor(frame / 4) % 3) + 1);
|
|
1165
|
+
}
|
|
1166
|
+
function createConversationName() {
|
|
1167
|
+
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+$/, "").replace("T", "-");
|
|
1168
|
+
return `thread-${stamp}`;
|
|
1169
|
+
}
|
|
1170
|
+
function userFacingError(error) {
|
|
1171
|
+
if (error instanceof Error)
|
|
1172
|
+
return error.message;
|
|
1173
|
+
return String(error);
|
|
1174
|
+
}
|
|
1175
|
+
function normalizeOptionalSetting(value, clearValues) {
|
|
1176
|
+
const trimmed = value.trim();
|
|
1177
|
+
if (!trimmed)
|
|
1178
|
+
return undefined;
|
|
1179
|
+
return clearValues.includes(trimmed.toLowerCase()) ? undefined : trimmed;
|
|
1180
|
+
}
|
|
1181
|
+
function runConfigText({ accessMode, contextEnabled, defaultPreset, profileName, runModel, runPreset, }) {
|
|
1182
|
+
return [
|
|
1183
|
+
`Profile: ${profileName}`,
|
|
1184
|
+
`Preset: ${runPreset || "none"}`,
|
|
1185
|
+
`Default preset: ${formatDefaultPreset(defaultPreset)}`,
|
|
1186
|
+
`Model: ${runModel || "auto"}`,
|
|
1187
|
+
`local_workdir tool: ${contextEnabled ? "on" : "off"}`,
|
|
1188
|
+
`local_shell tool: ${contextEnabled ? "on" : "off"}`,
|
|
1189
|
+
`Local access: ${accessMode}`,
|
|
1190
|
+
].join("\n");
|
|
1191
|
+
}
|
|
1192
|
+
function normalizeDefaultPreset(value) {
|
|
1193
|
+
const trimmed = value.trim();
|
|
1194
|
+
const lowered = trimmed.toLowerCase();
|
|
1195
|
+
if (!trimmed || lowered === "reset" || lowered === "default" || lowered === "builtin")
|
|
1196
|
+
return undefined;
|
|
1197
|
+
if (["none", "off", "disable", "disabled"].includes(lowered))
|
|
1198
|
+
return null;
|
|
1199
|
+
return trimmed;
|
|
1200
|
+
}
|
|
1201
|
+
function formatDefaultPreset(value) {
|
|
1202
|
+
if (value === undefined)
|
|
1203
|
+
return "built-in (pro-search)";
|
|
1204
|
+
return value ?? "none";
|
|
1205
|
+
}
|
|
1206
|
+
function effectiveDefaultPreset(preferences, builtInPreset) {
|
|
1207
|
+
if ("defaultPreset" in preferences)
|
|
1208
|
+
return preferences.defaultPreset ?? undefined;
|
|
1209
|
+
return builtInPreset;
|
|
1210
|
+
}
|
|
1211
|
+
function formatPresetList(presets) {
|
|
1212
|
+
if (presets.length === 0)
|
|
1213
|
+
return ["- none returned by this endpoint"];
|
|
1214
|
+
return presets.map((preset) => {
|
|
1215
|
+
const description = preset.description ? ` - ${preset.description}` : "";
|
|
1216
|
+
return `- ${preset.preset}${description}`;
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
function authStatusText(status) {
|
|
1220
|
+
return [
|
|
1221
|
+
`Profile: ${status.profile}`,
|
|
1222
|
+
`Endpoint: ${status.baseURL}`,
|
|
1223
|
+
`Auth: ${status.authType === "api_key" ? "API key" : "Browser session"}`,
|
|
1224
|
+
`Account: ${summarizeMe(status.me)}`,
|
|
1225
|
+
].join("\n");
|
|
1226
|
+
}
|
|
1227
|
+
function summarizeMe(me) {
|
|
1228
|
+
if (!me || typeof me !== "object")
|
|
1229
|
+
return "available";
|
|
1230
|
+
const obj = me;
|
|
1231
|
+
const candidates = [
|
|
1232
|
+
obj.email,
|
|
1233
|
+
obj.name,
|
|
1234
|
+
obj.username,
|
|
1235
|
+
obj.user_id,
|
|
1236
|
+
obj.id,
|
|
1237
|
+
nestedString(obj.user, "email"),
|
|
1238
|
+
nestedString(obj.user, "name"),
|
|
1239
|
+
nestedString(obj.user, "id"),
|
|
1240
|
+
];
|
|
1241
|
+
const picked = candidates.find((value) => typeof value === "string" && value.trim() !== "");
|
|
1242
|
+
return picked || "available";
|
|
1243
|
+
}
|
|
1244
|
+
function nestedString(value, key) {
|
|
1245
|
+
if (!value || typeof value !== "object")
|
|
1246
|
+
return undefined;
|
|
1247
|
+
const nested = value[key];
|
|
1248
|
+
return typeof nested === "string" ? nested : undefined;
|
|
1249
|
+
}
|
|
1250
|
+
async function applyLocalToolApproval(workdir, approval) {
|
|
1251
|
+
const workdirRegistry = createLocalWorkdirToolRegistry(workdir.workdir, { accessMode: "full" });
|
|
1252
|
+
const shellRegistry = createLocalShellToolRegistry({ workdir: workdir.workdir, accessMode: "full" });
|
|
1253
|
+
if (approval.name === workdirRegistry.toolName) {
|
|
1254
|
+
return await workdirRegistry.execute(approval.name, approval.arguments);
|
|
1255
|
+
}
|
|
1256
|
+
if (approval.name === shellRegistry.toolName) {
|
|
1257
|
+
return await shellRegistry.execute(approval.name, approval.arguments);
|
|
1258
|
+
}
|
|
1259
|
+
throw new Error(`no local handler registered for function ${approval.name}`);
|
|
1260
|
+
}
|
|
1261
|
+
function formatLocalToolApproval(approval) {
|
|
1262
|
+
return [
|
|
1263
|
+
`Local approval requested: ${approval.name}${approval.action ? `.${approval.action}` : ""}`,
|
|
1264
|
+
"Arguments:",
|
|
1265
|
+
JSON.stringify(approval.arguments, null, 2),
|
|
1266
|
+
approval.preview ? ["Preview:", JSON.stringify(approval.preview, null, 2)].join("\n") : "",
|
|
1267
|
+
"",
|
|
1268
|
+
"Use /apply to execute this action once, /apply-all to allow future local actions, or /reject to discard it.",
|
|
1269
|
+
].filter(Boolean).join("\n");
|
|
1270
|
+
}
|
|
1271
|
+
function pendingLocalLabel(state) {
|
|
1272
|
+
if (state.pendingLocalTool) {
|
|
1273
|
+
return `${state.pendingLocalTool.name}${state.pendingLocalTool.action ? `.${state.pendingLocalTool.action}` : ""}`;
|
|
1274
|
+
}
|
|
1275
|
+
return "none";
|
|
1276
|
+
}
|