@agent-api/cli 0.0.1 → 0.1.0
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 +11 -0
- package/dist/index.js +20 -2
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/tui/chat.d.ts +1 -5
- package/dist/tui/chat.js +1 -1276
- package/dist/tui/ink/app.d.ts +5 -0
- package/dist/tui/ink/app.js +308 -0
- package/dist/tui/ink/components.d.ts +10 -0
- package/dist/tui/ink/components.js +20 -0
- package/dist/tui/workbench.d.ts +30 -0
- package/dist/tui/workbench.js +101 -0
- package/dist/workbench/auth-controller.d.ts +43 -0
- package/dist/workbench/auth-controller.js +84 -0
- package/dist/workbench/auth-gate-controller.d.ts +62 -0
- package/dist/workbench/auth-gate-controller.js +231 -0
- package/dist/workbench/command-controller.d.ts +29 -0
- package/dist/workbench/command-controller.js +371 -0
- package/dist/workbench/conversation-controller.d.ts +32 -0
- package/dist/workbench/conversation-controller.js +53 -0
- package/dist/workbench/engine.d.ts +66 -0
- package/dist/workbench/engine.js +291 -0
- package/dist/workbench/input-controller.d.ts +44 -0
- package/dist/workbench/input-controller.js +71 -0
- package/dist/workbench/lifecycle-controller.d.ts +29 -0
- package/dist/workbench/lifecycle-controller.js +75 -0
- package/dist/workbench/local-controller.d.ts +19 -0
- package/dist/workbench/local-controller.js +89 -0
- package/dist/workbench/render-model.d.ts +46 -0
- package/dist/workbench/render-model.js +61 -0
- package/dist/workbench/runtime-controller.d.ts +12 -0
- package/dist/workbench/runtime-controller.js +57 -0
- package/dist/workbench/session.d.ts +27 -0
- package/dist/workbench/session.js +40 -0
- package/dist/workbench/settings-controller.d.ts +52 -0
- package/dist/workbench/settings-controller.js +120 -0
- package/dist/workbench/turn-controller.d.ts +25 -0
- package/dist/workbench/turn-controller.js +162 -0
- package/dist/workbench/view-model.d.ts +34 -0
- package/dist/workbench/view-model.js +121 -0
- package/package.json +2 -2
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
|
+
import { useApp, useInput, useStdout } from "ink";
|
|
4
|
+
import { defaultBaseURL } from "../../config.js";
|
|
5
|
+
import { createWorkbenchAuthController } from "../../workbench/auth-controller.js";
|
|
6
|
+
import { createWorkbenchAuthGateController, } from "../../workbench/auth-gate-controller.js";
|
|
7
|
+
import { createWorkbenchCommandController } from "../../workbench/command-controller.js";
|
|
8
|
+
import { buildWorkbenchRenderModel, } from "../../workbench/render-model.js";
|
|
9
|
+
import { createWorkbenchSession } from "../../workbench/session.js";
|
|
10
|
+
import { InkAuthGate, InkWorkbenchScreen } from "./components.js";
|
|
11
|
+
export function ChatApp({ options }) {
|
|
12
|
+
return _jsx(AuthenticatedChatApp, { options: options });
|
|
13
|
+
}
|
|
14
|
+
function AuthenticatedChatApp({ options }) {
|
|
15
|
+
const app = useApp();
|
|
16
|
+
const busyRef = useRef(false);
|
|
17
|
+
const authControllerRef = useRef(null);
|
|
18
|
+
const authGateControllerRef = useRef(null);
|
|
19
|
+
if (!authControllerRef.current) {
|
|
20
|
+
authControllerRef.current = createWorkbenchAuthController();
|
|
21
|
+
}
|
|
22
|
+
const authController = authControllerRef.current;
|
|
23
|
+
if (!authGateControllerRef.current) {
|
|
24
|
+
authGateControllerRef.current = createWorkbenchAuthGateController({ authController });
|
|
25
|
+
}
|
|
26
|
+
const authGateController = authGateControllerRef.current;
|
|
27
|
+
const [currentProfile, setCurrentProfile] = useState(options.profile || "default");
|
|
28
|
+
const [auth, setAuth] = useState(() => authGateController.initialState({
|
|
29
|
+
apiKey: process.env.AGENT_API_KEY || "",
|
|
30
|
+
baseURL: process.env.AGENT_API_BASE_URL || defaultBaseURL,
|
|
31
|
+
profile: options.profile || "default",
|
|
32
|
+
}));
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
let mounted = true;
|
|
35
|
+
authGateController.check(options.profile)
|
|
36
|
+
.then((result) => {
|
|
37
|
+
if (!mounted)
|
|
38
|
+
return;
|
|
39
|
+
if (result.profileName)
|
|
40
|
+
setCurrentProfile(result.profileName);
|
|
41
|
+
setAuth(result.state);
|
|
42
|
+
});
|
|
43
|
+
return () => {
|
|
44
|
+
mounted = false;
|
|
45
|
+
};
|
|
46
|
+
}, [authGateController, options.profile]);
|
|
47
|
+
useInput((input, key) => {
|
|
48
|
+
const result = authGateController.handleInput(input, key, auth);
|
|
49
|
+
if (result.state !== auth)
|
|
50
|
+
setAuth(result.state);
|
|
51
|
+
for (const effect of result.effects) {
|
|
52
|
+
switch (effect.type) {
|
|
53
|
+
case "exit":
|
|
54
|
+
app.exit();
|
|
55
|
+
break;
|
|
56
|
+
case "submit":
|
|
57
|
+
void submitAuthField();
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
async function submitAuthField() {
|
|
63
|
+
if (busyRef.current)
|
|
64
|
+
return;
|
|
65
|
+
busyRef.current = true;
|
|
66
|
+
try {
|
|
67
|
+
const result = await authGateController.submit(auth, {
|
|
68
|
+
onState: setAuth,
|
|
69
|
+
});
|
|
70
|
+
if (result.profileName)
|
|
71
|
+
setCurrentProfile(result.profileName);
|
|
72
|
+
setAuth(result.state);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
busyRef.current = false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (auth.status === "ready") {
|
|
79
|
+
return (_jsx(WorkbenchApp, { onLogin: () => {
|
|
80
|
+
setAuth((current) => authGateController.requestLogin(current, currentProfile));
|
|
81
|
+
}, onLogout: () => {
|
|
82
|
+
setAuth((current) => authGateController.requestLogout(current, currentProfile));
|
|
83
|
+
}, onDeleteProfile: async () => {
|
|
84
|
+
await authController.deleteProfile(currentProfile);
|
|
85
|
+
setAuth((current) => authGateController.deletedProfile(current, currentProfile));
|
|
86
|
+
}, onSwitchProfile: (name) => {
|
|
87
|
+
setAuth((current) => authGateController.requestSwitchProfile(current, currentProfile, name));
|
|
88
|
+
}, options: { ...options, profile: currentProfile }, profileName: currentProfile, authController: authController }));
|
|
89
|
+
}
|
|
90
|
+
return _jsx(InkAuthGate, { state: auth });
|
|
91
|
+
}
|
|
92
|
+
function WorkbenchApp({ authController, onLogin, onLogout, onDeleteProfile, onSwitchProfile, options, profileName, }) {
|
|
93
|
+
const app = useApp();
|
|
94
|
+
const { stdout } = useStdout();
|
|
95
|
+
const [draft, setDraft] = useState("");
|
|
96
|
+
const [spinnerFrame, setSpinnerFrame] = useState(0);
|
|
97
|
+
const [transcriptOffset, setTranscriptOffset] = useState(0);
|
|
98
|
+
const sessionRef = useRef(null);
|
|
99
|
+
if (!sessionRef.current) {
|
|
100
|
+
sessionRef.current = createWorkbenchSession({
|
|
101
|
+
authController,
|
|
102
|
+
baseOptions: options,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const session = sessionRef.current;
|
|
106
|
+
const engine = session.engine;
|
|
107
|
+
const conversationController = session.conversation;
|
|
108
|
+
const inputController = session.input;
|
|
109
|
+
const lifecycleController = session.lifecycle;
|
|
110
|
+
const localController = session.local;
|
|
111
|
+
const settingsController = session.settings;
|
|
112
|
+
const turnController = session.turn;
|
|
113
|
+
const state = useSyncExternalStore(engine.subscribe, engine.snapshot, engine.snapshot);
|
|
114
|
+
const dispatch = engine.dispatch;
|
|
115
|
+
const commandController = createWorkbenchCommandController({
|
|
116
|
+
authController,
|
|
117
|
+
conversationController,
|
|
118
|
+
engine,
|
|
119
|
+
localController,
|
|
120
|
+
options,
|
|
121
|
+
profileName,
|
|
122
|
+
settingsController,
|
|
123
|
+
turnController,
|
|
124
|
+
onDeleteProfile,
|
|
125
|
+
onExit: app.exit,
|
|
126
|
+
onLogin,
|
|
127
|
+
onLogout,
|
|
128
|
+
onSwitchProfile,
|
|
129
|
+
});
|
|
130
|
+
const renderModel = useMemo(() => buildWorkbenchRenderModel({
|
|
131
|
+
draft,
|
|
132
|
+
profileName,
|
|
133
|
+
spinnerFrame,
|
|
134
|
+
state,
|
|
135
|
+
transcriptOffset,
|
|
136
|
+
viewport: {
|
|
137
|
+
rows: stdout.rows || process.stdout.rows,
|
|
138
|
+
columns: stdout.columns || process.stdout.columns,
|
|
139
|
+
},
|
|
140
|
+
workdirFallback: options.workdir || process.cwd(),
|
|
141
|
+
}), [draft, options.workdir, profileName, spinnerFrame, state, stdout.columns, stdout.rows, transcriptOffset]);
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
setTranscriptOffset((offset) => Math.min(offset, renderModel.transcript.maxOffset));
|
|
144
|
+
}, [renderModel.transcript.maxOffset]);
|
|
145
|
+
function scrollTranscript(delta) {
|
|
146
|
+
setTranscriptOffset((offset) => Math.max(0, Math.min(renderModel.transcript.maxOffset, offset + delta)));
|
|
147
|
+
}
|
|
148
|
+
function scrollTranscriptToTop() {
|
|
149
|
+
setTranscriptOffset(renderModel.transcript.maxOffset);
|
|
150
|
+
}
|
|
151
|
+
function scrollTranscriptToBottom() {
|
|
152
|
+
setTranscriptOffset(0);
|
|
153
|
+
}
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
let mounted = true;
|
|
156
|
+
lifecycleController.maybeCheckForUpdate()
|
|
157
|
+
.then((effects) => {
|
|
158
|
+
if (mounted)
|
|
159
|
+
runLifecycleEffects(effects, () => mounted);
|
|
160
|
+
});
|
|
161
|
+
settingsController.loadInitial({
|
|
162
|
+
modelExplicit: options.modelExplicit,
|
|
163
|
+
preset: options.preset,
|
|
164
|
+
presetExplicit: options.presetExplicit,
|
|
165
|
+
})
|
|
166
|
+
.then((settings) => {
|
|
167
|
+
if (!mounted)
|
|
168
|
+
return;
|
|
169
|
+
dispatch({ type: "settings.set", settings });
|
|
170
|
+
})
|
|
171
|
+
.catch((error) => {
|
|
172
|
+
if (!mounted)
|
|
173
|
+
return;
|
|
174
|
+
dispatch({ type: "activity.add", level: "warning", text: `Config preferences unavailable: ${userFacingError(error)}` });
|
|
175
|
+
});
|
|
176
|
+
return () => {
|
|
177
|
+
mounted = false;
|
|
178
|
+
};
|
|
179
|
+
}, [dispatch, lifecycleController, options.modelExplicit, options.preset, options.presetExplicit, settingsController]);
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
let mounted = true;
|
|
182
|
+
dispatch({ type: "activity.add", text: "Loading workdir" });
|
|
183
|
+
localController.load(options.workdir || process.cwd())
|
|
184
|
+
.then((workdir) => {
|
|
185
|
+
if (!mounted)
|
|
186
|
+
return;
|
|
187
|
+
dispatch({
|
|
188
|
+
type: "workdir.set",
|
|
189
|
+
workdir,
|
|
190
|
+
});
|
|
191
|
+
})
|
|
192
|
+
.catch((error) => {
|
|
193
|
+
if (!mounted)
|
|
194
|
+
return;
|
|
195
|
+
dispatch({
|
|
196
|
+
type: "activity.add",
|
|
197
|
+
level: "error",
|
|
198
|
+
text: `Workdir unavailable: ${error instanceof Error ? error.message : String(error)}`,
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
return () => {
|
|
202
|
+
mounted = false;
|
|
203
|
+
};
|
|
204
|
+
}, [dispatch, localController, options.workdir]);
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
let mounted = true;
|
|
207
|
+
const refreshIntervalMs = 60_000;
|
|
208
|
+
const refreshAuth = async () => {
|
|
209
|
+
const effects = await lifecycleController.refreshAuth(options.profile);
|
|
210
|
+
if (mounted)
|
|
211
|
+
runLifecycleEffects(effects, () => mounted);
|
|
212
|
+
};
|
|
213
|
+
void refreshAuth();
|
|
214
|
+
const interval = setInterval(refreshAuth, refreshIntervalMs);
|
|
215
|
+
return () => {
|
|
216
|
+
mounted = false;
|
|
217
|
+
clearInterval(interval);
|
|
218
|
+
};
|
|
219
|
+
}, [lifecycleController, options.profile]);
|
|
220
|
+
useInput((input, key) => {
|
|
221
|
+
const result = inputController.handle(input, key, {
|
|
222
|
+
busy: state.busy,
|
|
223
|
+
draft,
|
|
224
|
+
viewportHeight: renderModel.viewportHeight,
|
|
225
|
+
});
|
|
226
|
+
if (result.draft !== draft)
|
|
227
|
+
setDraft(result.draft);
|
|
228
|
+
for (const effect of result.effects) {
|
|
229
|
+
switch (effect.type) {
|
|
230
|
+
case "exit":
|
|
231
|
+
app.exit();
|
|
232
|
+
break;
|
|
233
|
+
case "scroll":
|
|
234
|
+
scrollTranscript(effect.delta);
|
|
235
|
+
break;
|
|
236
|
+
case "scroll_top":
|
|
237
|
+
scrollTranscriptToTop();
|
|
238
|
+
break;
|
|
239
|
+
case "scroll_bottom":
|
|
240
|
+
scrollTranscriptToBottom();
|
|
241
|
+
break;
|
|
242
|
+
case "abort":
|
|
243
|
+
void turnController.abort("Abort requested.");
|
|
244
|
+
break;
|
|
245
|
+
case "submit":
|
|
246
|
+
void submit(effect.input);
|
|
247
|
+
break;
|
|
248
|
+
case "ignored_busy":
|
|
249
|
+
dispatch({ type: "message.add", role: "system", text: "Agent turn is running. Use /abort or Esc to cancel it." });
|
|
250
|
+
dispatch({ type: "activity.add", level: "warning", text: "Input ignored while agent is running" });
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
const initialPrompt = lifecycleController.initialPrompt({
|
|
257
|
+
busy: state.busy,
|
|
258
|
+
promptParts: options.promptParts,
|
|
259
|
+
workdir: state.workdir,
|
|
260
|
+
});
|
|
261
|
+
if (initialPrompt)
|
|
262
|
+
void turnController.startPrompt(initialPrompt);
|
|
263
|
+
}, [lifecycleController, options.promptParts, state.busy, state.workdir, turnController]);
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
if (!state.busy) {
|
|
266
|
+
setSpinnerFrame(0);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const interval = setInterval(() => {
|
|
270
|
+
setSpinnerFrame((frame) => frame + 1);
|
|
271
|
+
}, 120);
|
|
272
|
+
return () => clearInterval(interval);
|
|
273
|
+
}, [state.busy]);
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
return () => session.runtime.dispose();
|
|
276
|
+
}, [session.runtime]);
|
|
277
|
+
function runLifecycleEffects(effects, isMounted) {
|
|
278
|
+
for (const effect of effects) {
|
|
279
|
+
switch (effect.type) {
|
|
280
|
+
case "dispatch":
|
|
281
|
+
dispatch(effect.action);
|
|
282
|
+
break;
|
|
283
|
+
case "close":
|
|
284
|
+
setTimeout(() => {
|
|
285
|
+
if (isMounted())
|
|
286
|
+
app.exit();
|
|
287
|
+
}, effect.delayMs);
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async function submit(input) {
|
|
293
|
+
const submission = engine.submit(input);
|
|
294
|
+
if (submission.kind === "command") {
|
|
295
|
+
await commandController.run(submission.command);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (submission.kind === "prompt") {
|
|
299
|
+
await turnController.startPrompt(submission.prompt);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return _jsx(InkWorkbenchScreen, { renderModel: renderModel, spinnerFrame: spinnerFrame });
|
|
303
|
+
}
|
|
304
|
+
function userFacingError(error) {
|
|
305
|
+
if (error instanceof Error)
|
|
306
|
+
return error.message;
|
|
307
|
+
return String(error);
|
|
308
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type AuthGateState } from "../../workbench/auth-gate-controller.js";
|
|
3
|
+
import { type WorkbenchRenderModel } from "../../workbench/render-model.js";
|
|
4
|
+
export declare function InkWorkbenchScreen({ renderModel, spinnerFrame, }: {
|
|
5
|
+
renderModel: WorkbenchRenderModel;
|
|
6
|
+
spinnerFrame: number;
|
|
7
|
+
}): React.JSX.Element;
|
|
8
|
+
export declare function InkAuthGate({ state }: {
|
|
9
|
+
state: AuthGateState;
|
|
10
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { authMethods } from "../../workbench/auth-gate-controller.js";
|
|
4
|
+
import { busySpinner } from "../../workbench/render-model.js";
|
|
5
|
+
import { activityColor, } from "../workbench.js";
|
|
6
|
+
export function InkWorkbenchScreen({ renderModel, spinnerFrame, }) {
|
|
7
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { contextEnabled: renderModel.header.contextEnabled, conversation: renderModel.header.conversation, model: renderModel.header.model, accessMode: renderModel.header.accessMode, pendingLocalLabel: renderModel.header.pendingLocalLabel, preset: renderModel.header.preset, profile: renderModel.header.profile, renderMode: renderModel.header.renderMode, workdir: renderModel.header.workdir }), _jsxs(Box, { marginTop: 1, height: renderModel.viewportHeight, children: [_jsxs(Box, { flexDirection: "column", width: "72%", paddingRight: 1, children: [renderModel.transcript.visibleLines.map((line) => (_jsx(Text, { bold: line.bold, color: line.color, inverse: line.inverse, children: line.text || " " }, line.id))), renderModel.transcript.visibleLines.length === 0 && _jsx(Text, { color: "gray", children: "No transcript lines." })] }), _jsxs(Box, { flexDirection: "column", width: "28%", height: renderModel.activityHeight, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, children: "Activity" }), renderModel.visibleActivities.map((activity) => (_jsxs(Text, { color: activityColor(activity.level), children: [new Date(activity.timestamp).toLocaleTimeString(), " ", activity.text] }, activity.id)))] })] }), _jsxs(Box, { borderStyle: "single", borderColor: renderModel.input.busy ? "yellow" : "green", paddingX: 1, children: [renderModel.input.fullAccess && (_jsx(Text, { color: "red", bold: true, inverse: true, children: "FULL ACCESS" })), renderModel.input.fullAccess && _jsx(Text, { children: " " }), _jsxs(Text, { color: renderModel.input.busy ? "yellow" : "green", children: [renderModel.input.label, " "] }), renderModel.input.busy ? (_jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: busySpinner(spinnerFrame) }), " ", renderModel.input.waitingText] })) : (_jsxs(Text, { children: [renderModel.input.draft, _jsx(Cursor, { visible: true })] }))] }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { color: "gray", children: renderModel.footerText }) })] }));
|
|
8
|
+
}
|
|
9
|
+
export function InkAuthGate({ state }) {
|
|
10
|
+
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..." })] }))] })] }));
|
|
11
|
+
}
|
|
12
|
+
function AuthPrompt({ label, value }) {
|
|
13
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: "green", paddingX: 1, marginTop: 1, children: [_jsxs(Text, { color: "green", children: [label, ": "] }), _jsx(Text, { children: value })] }));
|
|
14
|
+
}
|
|
15
|
+
function Header({ contextEnabled, conversation, accessMode, model, pendingLocalLabel, preset, profile, renderMode, workdir, }) {
|
|
16
|
+
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", " render=", renderMode, " pending=", pendingLocalLabel] })] }));
|
|
17
|
+
}
|
|
18
|
+
function Cursor({ visible }) {
|
|
19
|
+
return visible ? _jsx(Text, { inverse: true, children: " " }) : _jsx(Text, { children: " " });
|
|
20
|
+
}
|
package/dist/tui/workbench.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export interface LocalToolApproval extends LocalToolApprovalRequest {
|
|
|
23
23
|
id: string;
|
|
24
24
|
createdAt: number;
|
|
25
25
|
}
|
|
26
|
+
export type RenderMode = "markdown" | "raw";
|
|
26
27
|
export interface WorkbenchState {
|
|
27
28
|
messages: WorkbenchMessage[];
|
|
28
29
|
activities: WorkbenchActivity[];
|
|
@@ -33,6 +34,17 @@ export interface WorkbenchState {
|
|
|
33
34
|
pendingLocalTool: LocalToolApproval | null;
|
|
34
35
|
accessMode: WorkdirAccessMode;
|
|
35
36
|
currentConversation: string;
|
|
37
|
+
runPreset?: string;
|
|
38
|
+
runModel?: string;
|
|
39
|
+
renderMode: RenderMode;
|
|
40
|
+
defaultPreset?: string | null;
|
|
41
|
+
}
|
|
42
|
+
export interface InputHistory {
|
|
43
|
+
record(value: string): void;
|
|
44
|
+
previous(currentDraft: string): string;
|
|
45
|
+
next(currentDraft: string): string;
|
|
46
|
+
reset(): void;
|
|
47
|
+
values(): string[];
|
|
36
48
|
}
|
|
37
49
|
export type WorkbenchAction = {
|
|
38
50
|
type: "message.add";
|
|
@@ -74,6 +86,9 @@ export type WorkbenchAction = {
|
|
|
74
86
|
} | {
|
|
75
87
|
type: "conversation.set";
|
|
76
88
|
name: string;
|
|
89
|
+
} | {
|
|
90
|
+
type: "settings.set";
|
|
91
|
+
settings: Partial<Pick<WorkbenchState, "runPreset" | "runModel" | "renderMode" | "defaultPreset">>;
|
|
77
92
|
};
|
|
78
93
|
export type WorkbenchCommand = {
|
|
79
94
|
kind: "invalid";
|
|
@@ -101,6 +116,14 @@ export type WorkbenchCommand = {
|
|
|
101
116
|
kind: "config";
|
|
102
117
|
field?: "preset";
|
|
103
118
|
value?: string;
|
|
119
|
+
} | {
|
|
120
|
+
kind: "render";
|
|
121
|
+
mode?: RenderMode;
|
|
122
|
+
} | {
|
|
123
|
+
kind: "transcript";
|
|
124
|
+
} | {
|
|
125
|
+
kind: "export";
|
|
126
|
+
path?: string;
|
|
104
127
|
} | {
|
|
105
128
|
kind: "context";
|
|
106
129
|
enabled?: boolean;
|
|
@@ -144,11 +167,18 @@ export declare function createInitialWorkbenchState(options: {
|
|
|
144
167
|
contextEnabled: boolean;
|
|
145
168
|
accessMode?: WorkdirAccessMode;
|
|
146
169
|
conversation?: string;
|
|
170
|
+
preset?: string;
|
|
171
|
+
model?: string;
|
|
172
|
+
renderMode?: RenderMode;
|
|
173
|
+
defaultPreset?: string | null;
|
|
147
174
|
}): WorkbenchState;
|
|
175
|
+
export declare function createInputHistory(limit?: number): InputHistory;
|
|
148
176
|
export declare function workbenchReducer(state: WorkbenchState, action: WorkbenchAction): WorkbenchState;
|
|
149
177
|
export declare function parseWorkbenchCommand(input: string): WorkbenchCommand | null;
|
|
150
178
|
export declare function parsePendingApprovalCommand(input: string): WorkbenchCommand | null;
|
|
151
179
|
export declare function helpText(): string;
|
|
180
|
+
export declare function formatTranscript(messages: WorkbenchMessage[]): string;
|
|
181
|
+
export declare function formatTranscriptPreview(messages: WorkbenchMessage[], maxLines?: number): string;
|
|
152
182
|
export declare function workdirText(status: WorkbenchWorkdirStatus | null): string;
|
|
153
183
|
export declare function formatBytes(bytes: number): string;
|
|
154
184
|
export declare function activityColor(level: ActivityLevel): "green" | "yellow" | "red" | "gray";
|
package/dist/tui/workbench.js
CHANGED
|
@@ -14,6 +14,60 @@ export function createInitialWorkbenchState(options) {
|
|
|
14
14
|
pendingLocalTool: null,
|
|
15
15
|
accessMode,
|
|
16
16
|
currentConversation: options.conversation || "default",
|
|
17
|
+
runPreset: options.preset,
|
|
18
|
+
runModel: options.model,
|
|
19
|
+
renderMode: options.renderMode ?? "markdown",
|
|
20
|
+
defaultPreset: options.defaultPreset,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function createInputHistory(limit = 100) {
|
|
24
|
+
const entries = [];
|
|
25
|
+
let cursor = null;
|
|
26
|
+
let draftBeforeBrowse = "";
|
|
27
|
+
return {
|
|
28
|
+
record(value) {
|
|
29
|
+
const trimmed = value.trim();
|
|
30
|
+
if (!trimmed)
|
|
31
|
+
return;
|
|
32
|
+
if (entries.at(-1) !== trimmed) {
|
|
33
|
+
entries.push(trimmed);
|
|
34
|
+
if (entries.length > limit)
|
|
35
|
+
entries.splice(0, entries.length - limit);
|
|
36
|
+
}
|
|
37
|
+
cursor = null;
|
|
38
|
+
draftBeforeBrowse = "";
|
|
39
|
+
},
|
|
40
|
+
previous(currentDraft) {
|
|
41
|
+
if (entries.length === 0)
|
|
42
|
+
return currentDraft;
|
|
43
|
+
if (cursor === null) {
|
|
44
|
+
draftBeforeBrowse = currentDraft;
|
|
45
|
+
cursor = entries.length - 1;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
cursor = Math.max(0, cursor - 1);
|
|
49
|
+
}
|
|
50
|
+
return entries[cursor] ?? currentDraft;
|
|
51
|
+
},
|
|
52
|
+
next(currentDraft) {
|
|
53
|
+
if (entries.length === 0 || cursor === null)
|
|
54
|
+
return currentDraft;
|
|
55
|
+
if (cursor < entries.length - 1) {
|
|
56
|
+
cursor += 1;
|
|
57
|
+
return entries[cursor] ?? currentDraft;
|
|
58
|
+
}
|
|
59
|
+
cursor = null;
|
|
60
|
+
const restored = draftBeforeBrowse;
|
|
61
|
+
draftBeforeBrowse = "";
|
|
62
|
+
return restored;
|
|
63
|
+
},
|
|
64
|
+
reset() {
|
|
65
|
+
cursor = null;
|
|
66
|
+
draftBeforeBrowse = "";
|
|
67
|
+
},
|
|
68
|
+
values() {
|
|
69
|
+
return [...entries];
|
|
70
|
+
},
|
|
17
71
|
};
|
|
18
72
|
}
|
|
19
73
|
export function workbenchReducer(state, action) {
|
|
@@ -80,6 +134,11 @@ export function workbenchReducer(state, action) {
|
|
|
80
134
|
currentConversation: action.name,
|
|
81
135
|
activities: [...state.activities, newActivity("info", `Conversation: ${action.name}`)].slice(-20),
|
|
82
136
|
};
|
|
137
|
+
case "settings.set":
|
|
138
|
+
return {
|
|
139
|
+
...state,
|
|
140
|
+
...action.settings,
|
|
141
|
+
};
|
|
83
142
|
default:
|
|
84
143
|
return state;
|
|
85
144
|
}
|
|
@@ -134,6 +193,18 @@ export function parseWorkbenchCommand(input) {
|
|
|
134
193
|
}
|
|
135
194
|
return { kind: "invalid", command: `${name} ${field}` };
|
|
136
195
|
}
|
|
196
|
+
case "render":
|
|
197
|
+
case "display":
|
|
198
|
+
case "view": {
|
|
199
|
+
const mode = rest[0];
|
|
200
|
+
if (mode === "raw" || mode === "markdown")
|
|
201
|
+
return { kind: "render", mode };
|
|
202
|
+
return { kind: "render" };
|
|
203
|
+
}
|
|
204
|
+
case "transcript":
|
|
205
|
+
return { kind: "transcript" };
|
|
206
|
+
case "export":
|
|
207
|
+
return { kind: "export", path: rest.join(" ").trim() || undefined };
|
|
137
208
|
case "context":
|
|
138
209
|
return { kind: "context", enabled: parseOnOff(rest[0]) };
|
|
139
210
|
case "access": {
|
|
@@ -215,6 +286,9 @@ export function helpText() {
|
|
|
215
286
|
"/switch-profile switch/sign in with a different profile",
|
|
216
287
|
"/delete-profile delete current saved profile and return to auth",
|
|
217
288
|
"/config show current run configuration and saved defaults",
|
|
289
|
+
"/render [mode] show or set output rendering: markdown or raw",
|
|
290
|
+
"/transcript show a plain-text transcript preview",
|
|
291
|
+
"/export [file] save the plain-text transcript to a file",
|
|
218
292
|
"/config preset save default preset; use none/off for no preset, reset for built-in",
|
|
219
293
|
"/preset [name] show or set preset; use none/off to clear",
|
|
220
294
|
"/model [name] show or set explicit model; use auto/none/off to clear",
|
|
@@ -236,6 +310,26 @@ export function helpText() {
|
|
|
236
310
|
"/quit leave the workbench",
|
|
237
311
|
].join("\n");
|
|
238
312
|
}
|
|
313
|
+
export function formatTranscript(messages) {
|
|
314
|
+
return messages
|
|
315
|
+
.map((message) => {
|
|
316
|
+
const body = message.text.trimEnd();
|
|
317
|
+
return body ? `${roleLabel(message.role)}:\n${body}` : `${roleLabel(message.role)}:`;
|
|
318
|
+
})
|
|
319
|
+
.join("\n\n")
|
|
320
|
+
.trimEnd() + "\n";
|
|
321
|
+
}
|
|
322
|
+
export function formatTranscriptPreview(messages, maxLines = 80) {
|
|
323
|
+
const lines = formatTranscript(messages).trimEnd().split(/\r?\n/);
|
|
324
|
+
if (lines.length <= maxLines) {
|
|
325
|
+
return ["Transcript preview:", "", ...lines].join("\n");
|
|
326
|
+
}
|
|
327
|
+
return [
|
|
328
|
+
`Transcript preview: showing last ${maxLines} lines of ${lines.length}. Use /export [file] for the full transcript.`,
|
|
329
|
+
"",
|
|
330
|
+
...lines.slice(-maxLines),
|
|
331
|
+
].join("\n");
|
|
332
|
+
}
|
|
239
333
|
function parseOnOff(value) {
|
|
240
334
|
if (!value)
|
|
241
335
|
return undefined;
|
|
@@ -275,6 +369,13 @@ export function activityColor(level) {
|
|
|
275
369
|
function newMessage(role, text, id = randomId()) {
|
|
276
370
|
return { id, role, text };
|
|
277
371
|
}
|
|
372
|
+
function roleLabel(role) {
|
|
373
|
+
if (role === "user")
|
|
374
|
+
return "You";
|
|
375
|
+
if (role === "assistant")
|
|
376
|
+
return "Agent";
|
|
377
|
+
return "System";
|
|
378
|
+
}
|
|
278
379
|
function newActivity(level, text) {
|
|
279
380
|
return {
|
|
280
381
|
id: randomId(),
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { deleteProfile, getAuthStatus, loginWithAPIKey, openBrowserURL, refreshActiveProfileIfNeeded, saveBrowserProfile, startBrowserAuthChallenge, waitForBrowserAuthChallenge, type AuthStatus } from "../profile.js";
|
|
2
|
+
export interface WorkbenchAuthController {
|
|
3
|
+
check(profile?: string, refreshWindowMs?: number): Promise<{
|
|
4
|
+
profileName: string;
|
|
5
|
+
refreshed: boolean;
|
|
6
|
+
}>;
|
|
7
|
+
loginAPIKey(input: {
|
|
8
|
+
profile: string;
|
|
9
|
+
baseURL: string;
|
|
10
|
+
apiKey: string;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
profileName: string;
|
|
13
|
+
}>;
|
|
14
|
+
loginBrowser(input: {
|
|
15
|
+
profile: string;
|
|
16
|
+
baseURL: string;
|
|
17
|
+
clientName?: string;
|
|
18
|
+
onChallenge?: (challenge: {
|
|
19
|
+
url: string;
|
|
20
|
+
code: string;
|
|
21
|
+
}) => void;
|
|
22
|
+
onStatus?: (status: string) => void;
|
|
23
|
+
}): Promise<{
|
|
24
|
+
profileName: string;
|
|
25
|
+
}>;
|
|
26
|
+
deleteProfile(name: string): Promise<void>;
|
|
27
|
+
statusText(profile?: string): Promise<string>;
|
|
28
|
+
refresh(profile?: string, refreshWindowMs?: number): Promise<{
|
|
29
|
+
refreshed: boolean;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
export interface WorkbenchAuthControllerOptions {
|
|
33
|
+
refreshActiveProfileIfNeededImpl?: typeof refreshActiveProfileIfNeeded;
|
|
34
|
+
loginWithAPIKeyImpl?: typeof loginWithAPIKey;
|
|
35
|
+
startBrowserAuthChallengeImpl?: typeof startBrowserAuthChallenge;
|
|
36
|
+
openBrowserURLImpl?: typeof openBrowserURL;
|
|
37
|
+
waitForBrowserAuthChallengeImpl?: typeof waitForBrowserAuthChallenge;
|
|
38
|
+
saveBrowserProfileImpl?: typeof saveBrowserProfile;
|
|
39
|
+
deleteProfileImpl?: typeof deleteProfile;
|
|
40
|
+
getAuthStatusImpl?: typeof getAuthStatus;
|
|
41
|
+
}
|
|
42
|
+
export declare function createWorkbenchAuthController(options?: WorkbenchAuthControllerOptions): WorkbenchAuthController;
|
|
43
|
+
export declare function authStatusText(status: AuthStatus): string;
|