@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.
Files changed (39) hide show
  1. package/dist/runtime/index.d.ts +1 -1
  2. package/dist/runtime/index.js +1 -1
  3. package/dist/tui/chat.d.ts +1 -6
  4. package/dist/tui/chat.js +1 -1292
  5. package/dist/tui/ink/app.d.ts +5 -0
  6. package/dist/tui/ink/app.js +308 -0
  7. package/dist/tui/ink/components.d.ts +10 -0
  8. package/dist/tui/ink/components.js +20 -0
  9. package/dist/tui/workbench.d.ts +22 -0
  10. package/dist/tui/workbench.js +51 -0
  11. package/dist/workbench/auth-controller.d.ts +43 -0
  12. package/dist/workbench/auth-controller.js +84 -0
  13. package/dist/workbench/auth-gate-controller.d.ts +62 -0
  14. package/dist/workbench/auth-gate-controller.js +231 -0
  15. package/dist/workbench/command-controller.d.ts +29 -0
  16. package/dist/workbench/command-controller.js +371 -0
  17. package/dist/workbench/conversation-controller.d.ts +32 -0
  18. package/dist/workbench/conversation-controller.js +53 -0
  19. package/dist/workbench/engine.d.ts +66 -0
  20. package/dist/workbench/engine.js +291 -0
  21. package/dist/workbench/input-controller.d.ts +44 -0
  22. package/dist/workbench/input-controller.js +71 -0
  23. package/dist/workbench/lifecycle-controller.d.ts +29 -0
  24. package/dist/workbench/lifecycle-controller.js +75 -0
  25. package/dist/workbench/local-controller.d.ts +19 -0
  26. package/dist/workbench/local-controller.js +89 -0
  27. package/dist/workbench/render-model.d.ts +46 -0
  28. package/dist/workbench/render-model.js +61 -0
  29. package/dist/workbench/runtime-controller.d.ts +12 -0
  30. package/dist/workbench/runtime-controller.js +57 -0
  31. package/dist/workbench/session.d.ts +27 -0
  32. package/dist/workbench/session.js +40 -0
  33. package/dist/workbench/settings-controller.d.ts +52 -0
  34. package/dist/workbench/settings-controller.js +120 -0
  35. package/dist/workbench/turn-controller.d.ts +25 -0
  36. package/dist/workbench/turn-controller.js +162 -0
  37. package/dist/workbench/view-model.d.ts +34 -0
  38. package/dist/workbench/view-model.js +121 -0
  39. package/package.json +1 -1
@@ -0,0 +1,5 @@
1
+ import React from "react";
2
+ import { type AgentRunOptions } from "../../agent.js";
3
+ export declare function ChatApp({ options }: {
4
+ options: AgentRunOptions;
5
+ }): React.JSX.Element;
@@ -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
+ }
@@ -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";
@@ -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
+ }