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