@agent-api/cli 0.0.3 → 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/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/tui/chat.d.ts +1 -6
- package/dist/tui/chat.js +1 -1292
- 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 +22 -0
- package/dist/tui/workbench.js +51 -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 +1 -1
|
@@ -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,10 @@ 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;
|
|
36
41
|
}
|
|
37
42
|
export interface InputHistory {
|
|
38
43
|
record(value: string): void;
|
|
@@ -81,6 +86,9 @@ export type WorkbenchAction = {
|
|
|
81
86
|
} | {
|
|
82
87
|
type: "conversation.set";
|
|
83
88
|
name: string;
|
|
89
|
+
} | {
|
|
90
|
+
type: "settings.set";
|
|
91
|
+
settings: Partial<Pick<WorkbenchState, "runPreset" | "runModel" | "renderMode" | "defaultPreset">>;
|
|
84
92
|
};
|
|
85
93
|
export type WorkbenchCommand = {
|
|
86
94
|
kind: "invalid";
|
|
@@ -108,6 +116,14 @@ export type WorkbenchCommand = {
|
|
|
108
116
|
kind: "config";
|
|
109
117
|
field?: "preset";
|
|
110
118
|
value?: string;
|
|
119
|
+
} | {
|
|
120
|
+
kind: "render";
|
|
121
|
+
mode?: RenderMode;
|
|
122
|
+
} | {
|
|
123
|
+
kind: "transcript";
|
|
124
|
+
} | {
|
|
125
|
+
kind: "export";
|
|
126
|
+
path?: string;
|
|
111
127
|
} | {
|
|
112
128
|
kind: "context";
|
|
113
129
|
enabled?: boolean;
|
|
@@ -151,12 +167,18 @@ export declare function createInitialWorkbenchState(options: {
|
|
|
151
167
|
contextEnabled: boolean;
|
|
152
168
|
accessMode?: WorkdirAccessMode;
|
|
153
169
|
conversation?: string;
|
|
170
|
+
preset?: string;
|
|
171
|
+
model?: string;
|
|
172
|
+
renderMode?: RenderMode;
|
|
173
|
+
defaultPreset?: string | null;
|
|
154
174
|
}): WorkbenchState;
|
|
155
175
|
export declare function createInputHistory(limit?: number): InputHistory;
|
|
156
176
|
export declare function workbenchReducer(state: WorkbenchState, action: WorkbenchAction): WorkbenchState;
|
|
157
177
|
export declare function parseWorkbenchCommand(input: string): WorkbenchCommand | null;
|
|
158
178
|
export declare function parsePendingApprovalCommand(input: string): WorkbenchCommand | null;
|
|
159
179
|
export declare function helpText(): string;
|
|
180
|
+
export declare function formatTranscript(messages: WorkbenchMessage[]): string;
|
|
181
|
+
export declare function formatTranscriptPreview(messages: WorkbenchMessage[], maxLines?: number): string;
|
|
160
182
|
export declare function workdirText(status: WorkbenchWorkdirStatus | null): string;
|
|
161
183
|
export declare function formatBytes(bytes: number): string;
|
|
162
184
|
export declare function activityColor(level: ActivityLevel): "green" | "yellow" | "red" | "gray";
|
package/dist/tui/workbench.js
CHANGED
|
@@ -14,6 +14,10 @@ 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,
|
|
17
21
|
};
|
|
18
22
|
}
|
|
19
23
|
export function createInputHistory(limit = 100) {
|
|
@@ -130,6 +134,11 @@ export function workbenchReducer(state, action) {
|
|
|
130
134
|
currentConversation: action.name,
|
|
131
135
|
activities: [...state.activities, newActivity("info", `Conversation: ${action.name}`)].slice(-20),
|
|
132
136
|
};
|
|
137
|
+
case "settings.set":
|
|
138
|
+
return {
|
|
139
|
+
...state,
|
|
140
|
+
...action.settings,
|
|
141
|
+
};
|
|
133
142
|
default:
|
|
134
143
|
return state;
|
|
135
144
|
}
|
|
@@ -184,6 +193,18 @@ export function parseWorkbenchCommand(input) {
|
|
|
184
193
|
}
|
|
185
194
|
return { kind: "invalid", command: `${name} ${field}` };
|
|
186
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 };
|
|
187
208
|
case "context":
|
|
188
209
|
return { kind: "context", enabled: parseOnOff(rest[0]) };
|
|
189
210
|
case "access": {
|
|
@@ -265,6 +286,9 @@ export function helpText() {
|
|
|
265
286
|
"/switch-profile switch/sign in with a different profile",
|
|
266
287
|
"/delete-profile delete current saved profile and return to auth",
|
|
267
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",
|
|
268
292
|
"/config preset save default preset; use none/off for no preset, reset for built-in",
|
|
269
293
|
"/preset [name] show or set preset; use none/off to clear",
|
|
270
294
|
"/model [name] show or set explicit model; use auto/none/off to clear",
|
|
@@ -286,6 +310,26 @@ export function helpText() {
|
|
|
286
310
|
"/quit leave the workbench",
|
|
287
311
|
].join("\n");
|
|
288
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
|
+
}
|
|
289
333
|
function parseOnOff(value) {
|
|
290
334
|
if (!value)
|
|
291
335
|
return undefined;
|
|
@@ -325,6 +369,13 @@ export function activityColor(level) {
|
|
|
325
369
|
function newMessage(role, text, id = randomId()) {
|
|
326
370
|
return { id, role, text };
|
|
327
371
|
}
|
|
372
|
+
function roleLabel(role) {
|
|
373
|
+
if (role === "user")
|
|
374
|
+
return "You";
|
|
375
|
+
if (role === "assistant")
|
|
376
|
+
return "Agent";
|
|
377
|
+
return "System";
|
|
378
|
+
}
|
|
328
379
|
function newActivity(level, text) {
|
|
329
380
|
return {
|
|
330
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;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { deleteProfile, formatDeviceUserCode, getAuthStatus, loginWithAPIKey, openBrowserURL, refreshActiveProfileIfNeeded, saveBrowserProfile, startBrowserAuthChallenge, waitForBrowserAuthChallenge, } from "../profile.js";
|
|
2
|
+
export function createWorkbenchAuthController(options = {}) {
|
|
3
|
+
const refreshActiveProfileIfNeededImpl = options.refreshActiveProfileIfNeededImpl ?? refreshActiveProfileIfNeeded;
|
|
4
|
+
const loginWithAPIKeyImpl = options.loginWithAPIKeyImpl ?? loginWithAPIKey;
|
|
5
|
+
const startBrowserAuthChallengeImpl = options.startBrowserAuthChallengeImpl ?? startBrowserAuthChallenge;
|
|
6
|
+
const openBrowserURLImpl = options.openBrowserURLImpl ?? openBrowserURL;
|
|
7
|
+
const waitForBrowserAuthChallengeImpl = options.waitForBrowserAuthChallengeImpl ?? waitForBrowserAuthChallenge;
|
|
8
|
+
const saveBrowserProfileImpl = options.saveBrowserProfileImpl ?? saveBrowserProfile;
|
|
9
|
+
const deleteProfileImpl = options.deleteProfileImpl ?? deleteProfile;
|
|
10
|
+
const getAuthStatusImpl = options.getAuthStatusImpl ?? getAuthStatus;
|
|
11
|
+
return {
|
|
12
|
+
async check(profile, refreshWindowMs = 5 * 60_000) {
|
|
13
|
+
const result = await refreshActiveProfileIfNeededImpl(profile, refreshWindowMs);
|
|
14
|
+
return { profileName: result.profile.name, refreshed: result.refreshed };
|
|
15
|
+
},
|
|
16
|
+
async loginAPIKey(input) {
|
|
17
|
+
const saved = await loginWithAPIKeyImpl(input);
|
|
18
|
+
return { profileName: saved.name };
|
|
19
|
+
},
|
|
20
|
+
async loginBrowser(input) {
|
|
21
|
+
const challenge = await startBrowserAuthChallengeImpl({
|
|
22
|
+
baseURL: input.baseURL,
|
|
23
|
+
clientName: input.clientName || "Agent API TUI",
|
|
24
|
+
});
|
|
25
|
+
input.onChallenge?.({
|
|
26
|
+
url: challenge.verification_uri_complete,
|
|
27
|
+
code: formatDeviceUserCode(challenge.user_code),
|
|
28
|
+
});
|
|
29
|
+
await openBrowserURLImpl(challenge.verification_uri_complete).catch(() => undefined);
|
|
30
|
+
const session = await waitForBrowserAuthChallengeImpl({
|
|
31
|
+
baseURL: input.baseURL,
|
|
32
|
+
challenge,
|
|
33
|
+
on_poll(result) {
|
|
34
|
+
if (result.status && result.status !== "pending") {
|
|
35
|
+
input.onStatus?.(result.status);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
const saved = await saveBrowserProfileImpl(input.profile, input.baseURL, session);
|
|
40
|
+
return { profileName: saved.name };
|
|
41
|
+
},
|
|
42
|
+
async deleteProfile(name) {
|
|
43
|
+
await deleteProfileImpl(name);
|
|
44
|
+
},
|
|
45
|
+
async statusText(profile) {
|
|
46
|
+
return authStatusText(await getAuthStatusImpl(profile));
|
|
47
|
+
},
|
|
48
|
+
async refresh(profile, refreshWindowMs = 5 * 60_000) {
|
|
49
|
+
const result = await refreshActiveProfileIfNeededImpl(profile, refreshWindowMs);
|
|
50
|
+
return { refreshed: result.refreshed };
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function authStatusText(status) {
|
|
55
|
+
return [
|
|
56
|
+
`Profile: ${status.profile}`,
|
|
57
|
+
`Endpoint: ${status.baseURL}`,
|
|
58
|
+
`Auth: ${status.authType === "api_key" ? "API key" : "Browser session"}`,
|
|
59
|
+
`Account: ${summarizeMe(status.me)}`,
|
|
60
|
+
].join("\n");
|
|
61
|
+
}
|
|
62
|
+
function summarizeMe(me) {
|
|
63
|
+
if (!me || typeof me !== "object")
|
|
64
|
+
return "available";
|
|
65
|
+
const obj = me;
|
|
66
|
+
const candidates = [
|
|
67
|
+
obj.email,
|
|
68
|
+
obj.name,
|
|
69
|
+
obj.username,
|
|
70
|
+
obj.user_id,
|
|
71
|
+
obj.id,
|
|
72
|
+
nestedString(obj.user, "email"),
|
|
73
|
+
nestedString(obj.user, "name"),
|
|
74
|
+
nestedString(obj.user, "id"),
|
|
75
|
+
];
|
|
76
|
+
const picked = candidates.find((value) => typeof value === "string" && value.trim() !== "");
|
|
77
|
+
return picked || "available";
|
|
78
|
+
}
|
|
79
|
+
function nestedString(value, key) {
|
|
80
|
+
if (!value || typeof value !== "object")
|
|
81
|
+
return undefined;
|
|
82
|
+
const nested = value[key];
|
|
83
|
+
return typeof nested === "string" ? nested : undefined;
|
|
84
|
+
}
|