@agent-api/cli 0.4.21 → 0.4.22
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.d.ts +1 -1
- package/dist/runtime.js +1 -1
- package/dist/tui/clipboard.d.ts +1 -0
- package/dist/tui/clipboard.js +48 -0
- package/dist/tui/ink/app.js +122 -41
- package/dist/tui/ink/components.d.ts +7 -2
- package/dist/tui/ink/components.js +76 -7
- package/dist/tui/mouse.d.ts +4 -0
- package/dist/tui/mouse.js +30 -0
- package/package.json +2 -2
package/dist/runtime.d.ts
CHANGED
package/dist/runtime.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function writeClipboard(text: string, stdout?: NodeJS.WriteStream): Promise<boolean>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { platform } from "node:os";
|
|
3
|
+
export async function writeClipboard(text, stdout = process.stdout) {
|
|
4
|
+
const osc52Written = writeOsc52(text, stdout);
|
|
5
|
+
const nativeWritten = await writeNativeClipboard(text);
|
|
6
|
+
return osc52Written || nativeWritten;
|
|
7
|
+
}
|
|
8
|
+
function writeOsc52(text, stdout) {
|
|
9
|
+
if (!stdout.isTTY)
|
|
10
|
+
return false;
|
|
11
|
+
const sequence = `\x1b]52;c;${Buffer.from(text).toString("base64")}\x07`;
|
|
12
|
+
stdout.write(process.env.TMUX || process.env.STY ? `\x1bPtmux;\x1b${sequence}\x1b\\` : sequence);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
async function writeNativeClipboard(text) {
|
|
16
|
+
const commands = nativeClipboardCommands();
|
|
17
|
+
for (const [command, args] of commands) {
|
|
18
|
+
if (await writeCommand(command, args, text))
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
function nativeClipboardCommands() {
|
|
24
|
+
if (platform() === "darwin")
|
|
25
|
+
return [["pbcopy", []]];
|
|
26
|
+
if (platform() === "win32") {
|
|
27
|
+
return [["powershell.exe", [
|
|
28
|
+
"-NonInteractive",
|
|
29
|
+
"-NoProfile",
|
|
30
|
+
"-Command",
|
|
31
|
+
"[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())",
|
|
32
|
+
]]];
|
|
33
|
+
}
|
|
34
|
+
const commands = [];
|
|
35
|
+
if (process.env.WAYLAND_DISPLAY)
|
|
36
|
+
commands.push(["wl-copy", []]);
|
|
37
|
+
commands.push(["xclip", ["-selection", "clipboard"]]);
|
|
38
|
+
commands.push(["xsel", ["--clipboard", "--input"]]);
|
|
39
|
+
return commands;
|
|
40
|
+
}
|
|
41
|
+
function writeCommand(command, args, input) {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
const child = spawn(command, args, { stdio: ["pipe", "ignore", "ignore"] });
|
|
44
|
+
child.on("error", () => resolve(false));
|
|
45
|
+
child.on("close", (code) => resolve(code === 0));
|
|
46
|
+
child.stdin.end(input);
|
|
47
|
+
});
|
|
48
|
+
}
|
package/dist/tui/ink/app.js
CHANGED
|
@@ -2,9 +2,11 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
3
|
import { useApp, useInput, useStdout } from "ink";
|
|
4
4
|
import { createAgentEngine, defaultBaseURL, } from "@agent-api/app-engine/core";
|
|
5
|
-
import { createWorkbenchAuthController, createWorkbenchAuthGateController, } from "@agent-api/app-engine/workbench";
|
|
6
|
-
import { buildWorkbenchRenderModel,
|
|
5
|
+
import { createWorkbenchAuthController, createWorkbenchAuthGateController, parseWorkbenchCommand, } from "@agent-api/app-engine/workbench";
|
|
6
|
+
import { buildWorkbenchRenderModel, copyTextFromActivitySelection, copyTextFromRenderModel, copyTextFromTranscriptSelection, createWorkbenchTerminalController, initialWorkbenchTerminalState, normalizeTerminalState, selectedPanelRange, } from "@agent-api/app-engine/terminal";
|
|
7
7
|
import { InkAuthGate, InkWorkbenchScreen } from "./components.js";
|
|
8
|
+
import { writeClipboard } from "../clipboard.js";
|
|
9
|
+
import { disableMouseReporting, enableMouseReporting, parseMouseEvent } from "../mouse.js";
|
|
8
10
|
export function ChatApp({ options }) {
|
|
9
11
|
return _jsx(AuthenticatedChatApp, { options: options });
|
|
10
12
|
}
|
|
@@ -31,7 +33,11 @@ function AuthenticatedChatApp({ options }) {
|
|
|
31
33
|
}));
|
|
32
34
|
useEffect(() => {
|
|
33
35
|
hideTerminalCursor(stdout);
|
|
34
|
-
|
|
36
|
+
enableTerminalMouse(stdout);
|
|
37
|
+
return () => {
|
|
38
|
+
disableTerminalMouse(stdout);
|
|
39
|
+
showTerminalCursor(stdout);
|
|
40
|
+
};
|
|
35
41
|
}, [stdout]);
|
|
36
42
|
useEffect(() => {
|
|
37
43
|
let mounted = true;
|
|
@@ -58,6 +64,8 @@ function AuthenticatedChatApp({ options }) {
|
|
|
58
64
|
return () => clearInterval(interval);
|
|
59
65
|
}, [auth.status]);
|
|
60
66
|
useInput((input, key) => {
|
|
67
|
+
if (parseMouseEvent(input))
|
|
68
|
+
return;
|
|
61
69
|
const result = authGateController.handleInput(input, key, auth);
|
|
62
70
|
if (result.state !== auth)
|
|
63
71
|
setAuth(result.state);
|
|
@@ -110,6 +118,14 @@ function showTerminalCursor(stdout) {
|
|
|
110
118
|
if (stdout.isTTY)
|
|
111
119
|
stdout.write("\x1b[?25h");
|
|
112
120
|
}
|
|
121
|
+
function enableTerminalMouse(stdout) {
|
|
122
|
+
if (stdout.isTTY)
|
|
123
|
+
stdout.write(enableMouseReporting);
|
|
124
|
+
}
|
|
125
|
+
function disableTerminalMouse(stdout) {
|
|
126
|
+
if (stdout.isTTY)
|
|
127
|
+
stdout.write(disableMouseReporting);
|
|
128
|
+
}
|
|
113
129
|
function isAuthInputStatus(status) {
|
|
114
130
|
return status === "api_profile"
|
|
115
131
|
|| status === "api_base_url"
|
|
@@ -121,11 +137,8 @@ function WorkbenchApp({ authController, onLogin, onLogout, onDeleteProfile, onSw
|
|
|
121
137
|
const app = useApp();
|
|
122
138
|
const { stdout } = useStdout();
|
|
123
139
|
const terminalSize = useTerminalSize(stdout);
|
|
124
|
-
const [
|
|
125
|
-
const [cursor, setCursor] = useState(0);
|
|
126
|
-
const [selectionAnchor, setSelectionAnchor] = useState(null);
|
|
140
|
+
const [terminalState, setTerminalState] = useState(() => initialWorkbenchTerminalState());
|
|
127
141
|
const [spinnerFrame, setSpinnerFrame] = useState(0);
|
|
128
|
-
const [transcriptOffset, setTranscriptOffset] = useState(0);
|
|
129
142
|
const agentEngineRef = useRef(null);
|
|
130
143
|
if (!agentEngineRef.current) {
|
|
131
144
|
agentEngineRef.current = createAgentEngine({
|
|
@@ -140,38 +153,71 @@ function WorkbenchApp({ authController, onLogin, onLogout, onDeleteProfile, onSw
|
|
|
140
153
|
});
|
|
141
154
|
}
|
|
142
155
|
const agentEngine = agentEngineRef.current;
|
|
143
|
-
const
|
|
144
|
-
if (!
|
|
145
|
-
|
|
156
|
+
const terminalControllerRef = useRef(null);
|
|
157
|
+
if (!terminalControllerRef.current) {
|
|
158
|
+
terminalControllerRef.current = createWorkbenchTerminalController();
|
|
146
159
|
}
|
|
147
|
-
const
|
|
160
|
+
const terminalController = terminalControllerRef.current;
|
|
148
161
|
const state = useSyncExternalStore(agentEngine.subscribe, agentEngine.snapshot, agentEngine.snapshot);
|
|
149
162
|
const dispatch = agentEngine.dispatch;
|
|
150
163
|
const renderModel = useMemo(() => buildWorkbenchRenderModel({
|
|
151
|
-
draft,
|
|
152
|
-
cursor,
|
|
164
|
+
draft: terminalState.draft,
|
|
165
|
+
cursor: terminalState.cursor,
|
|
153
166
|
profileName,
|
|
154
|
-
selectionAnchor,
|
|
167
|
+
selectionAnchor: terminalState.selectionAnchor,
|
|
155
168
|
spinnerFrame,
|
|
156
169
|
state,
|
|
157
|
-
transcriptOffset,
|
|
170
|
+
transcriptOffset: terminalState.transcriptOffset,
|
|
158
171
|
viewport: {
|
|
159
172
|
rows: terminalSize.rows,
|
|
160
173
|
columns: terminalSize.columns,
|
|
161
174
|
},
|
|
162
175
|
workdirFallback: options.workdir || process.cwd(),
|
|
163
|
-
}), [
|
|
176
|
+
}), [options.workdir, profileName, spinnerFrame, state, terminalSize.columns, terminalSize.rows, terminalState.cursor, terminalState.draft, terminalState.selectionAnchor, terminalState.transcriptOffset]);
|
|
164
177
|
useEffect(() => {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
178
|
+
setTerminalState((current) => {
|
|
179
|
+
const next = normalizeTerminalState(current, renderModel);
|
|
180
|
+
return sameTerminalState(current, next) ? current : next;
|
|
181
|
+
});
|
|
182
|
+
}, [renderModel]);
|
|
183
|
+
async function submitInput(input) {
|
|
184
|
+
const command = parseWorkbenchCommand(input);
|
|
185
|
+
if (command?.kind === "copy") {
|
|
186
|
+
await copyPanelText(command.target);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
await agentEngine.submit(input);
|
|
169
190
|
}
|
|
170
|
-
function
|
|
171
|
-
|
|
191
|
+
async function copyPanelText(target) {
|
|
192
|
+
const text = copyTextForTarget(target);
|
|
193
|
+
if (!text) {
|
|
194
|
+
dispatch({ type: "activity.add", level: "warning", text: `Nothing to copy: ${target}` });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const copied = await writeClipboard(text, stdout);
|
|
199
|
+
dispatch({
|
|
200
|
+
type: "activity.add",
|
|
201
|
+
level: copied ? "success" : "warning",
|
|
202
|
+
text: copied ? `Copied ${target} to clipboard` : `Clipboard unavailable for ${target}`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
dispatch({ type: "activity.add", level: "error", text: `Copy failed: ${userFacingError(error)}` });
|
|
207
|
+
}
|
|
172
208
|
}
|
|
173
|
-
function
|
|
174
|
-
|
|
209
|
+
function copyTextForTarget(target) {
|
|
210
|
+
if (target === "transcript" || target === "page") {
|
|
211
|
+
const selection = selectedPanelRange(terminalState.transcriptSelectionAnchor, terminalState.transcriptCursor);
|
|
212
|
+
if (selection)
|
|
213
|
+
return copyTextFromTranscriptSelection(renderModel.transcript.lines, selection);
|
|
214
|
+
}
|
|
215
|
+
if (target === "activity") {
|
|
216
|
+
const selection = selectedPanelRange(terminalState.activitySelectionAnchor, terminalState.activityCursor);
|
|
217
|
+
if (selection)
|
|
218
|
+
return copyTextFromActivitySelection(renderModel.visibleActivities, selection);
|
|
219
|
+
}
|
|
220
|
+
return copyTextFromRenderModel(renderModel, target);
|
|
175
221
|
}
|
|
176
222
|
useEffect(() => {
|
|
177
223
|
let mounted = true;
|
|
@@ -206,44 +252,55 @@ function WorkbenchApp({ authController, onLogin, onLogout, onDeleteProfile, onSw
|
|
|
206
252
|
};
|
|
207
253
|
}, [agentEngine, options.profile]);
|
|
208
254
|
useInput((input, key) => {
|
|
209
|
-
const
|
|
255
|
+
const mouse = parseMouseEvent(input);
|
|
256
|
+
if (mouse) {
|
|
257
|
+
const result = terminalController.handleMouse(mouse, terminalState, {
|
|
258
|
+
busy: state.busy,
|
|
259
|
+
renderModel,
|
|
260
|
+
});
|
|
261
|
+
if (!sameTerminalState(result.state, terminalState))
|
|
262
|
+
setTerminalState(result.state);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const result = terminalController.handle(input, key, terminalState, {
|
|
210
266
|
busy: state.busy,
|
|
211
|
-
|
|
212
|
-
draft,
|
|
213
|
-
selectionAnchor,
|
|
214
|
-
viewportColumns: renderModel.input.viewportColumns,
|
|
215
|
-
viewportHeight: renderModel.viewportHeight,
|
|
267
|
+
renderModel,
|
|
216
268
|
});
|
|
217
|
-
if (result.
|
|
218
|
-
|
|
219
|
-
if (result.cursor !== cursor)
|
|
220
|
-
setCursor(result.cursor);
|
|
221
|
-
if (result.selectionAnchor !== selectionAnchor)
|
|
222
|
-
setSelectionAnchor(result.selectionAnchor);
|
|
269
|
+
if (!sameTerminalState(result.state, terminalState))
|
|
270
|
+
setTerminalState(result.state);
|
|
223
271
|
for (const effect of result.effects) {
|
|
224
272
|
switch (effect.type) {
|
|
225
273
|
case "exit":
|
|
226
274
|
app.exit();
|
|
227
275
|
break;
|
|
228
276
|
case "scroll":
|
|
229
|
-
|
|
277
|
+
setTerminalState((current) => normalizeTerminalState({
|
|
278
|
+
...current,
|
|
279
|
+
transcriptOffset: current.transcriptOffset + effect.delta,
|
|
280
|
+
}, renderModel));
|
|
230
281
|
break;
|
|
231
282
|
case "scroll_top":
|
|
232
|
-
|
|
283
|
+
setTerminalState((current) => normalizeTerminalState({
|
|
284
|
+
...current,
|
|
285
|
+
transcriptOffset: renderModel.transcript.maxOffset,
|
|
286
|
+
}, renderModel));
|
|
233
287
|
break;
|
|
234
288
|
case "scroll_bottom":
|
|
235
|
-
|
|
289
|
+
setTerminalState((current) => ({ ...current, transcriptOffset: 0 }));
|
|
236
290
|
break;
|
|
237
291
|
case "abort":
|
|
238
292
|
void agentEngine.abortActiveTurn("Abort requested.");
|
|
239
293
|
break;
|
|
240
294
|
case "submit":
|
|
241
|
-
void
|
|
295
|
+
void submitInput(effect.input);
|
|
242
296
|
break;
|
|
243
297
|
case "ignored_busy":
|
|
244
298
|
dispatch({ type: "message.add", role: "system", text: "Agent turn is running. Use /abort or Esc to cancel it." });
|
|
245
299
|
dispatch({ type: "activity.add", level: "warning", text: "Input ignored while agent is running" });
|
|
246
300
|
break;
|
|
301
|
+
case "copy":
|
|
302
|
+
void copyPanelText(effect.target);
|
|
303
|
+
break;
|
|
247
304
|
}
|
|
248
305
|
}
|
|
249
306
|
});
|
|
@@ -263,7 +320,7 @@ function WorkbenchApp({ authController, onLogin, onLogout, onDeleteProfile, onSw
|
|
|
263
320
|
useEffect(() => {
|
|
264
321
|
return () => agentEngine.dispose();
|
|
265
322
|
}, [agentEngine]);
|
|
266
|
-
return _jsx(InkWorkbenchScreen, { renderModel: renderModel, spinnerFrame: spinnerFrame });
|
|
323
|
+
return (_jsx(InkWorkbenchScreen, { activityCursor: terminalState.activityCursor, activitySelection: selectedPanelRange(terminalState.activitySelectionAnchor, terminalState.activityCursor), focusedPanel: terminalState.focusedPanel, renderModel: renderModel, spinnerFrame: spinnerFrame, transcriptCursor: terminalState.transcriptCursor, transcriptSelection: selectedPanelRange(terminalState.transcriptSelectionAnchor, terminalState.transcriptCursor) }));
|
|
267
324
|
}
|
|
268
325
|
function useTerminalSize(stdout) {
|
|
269
326
|
const [size, setSize] = useState(() => ({
|
|
@@ -292,3 +349,27 @@ function useTerminalSize(stdout) {
|
|
|
292
349
|
}, [stdout]);
|
|
293
350
|
return size;
|
|
294
351
|
}
|
|
352
|
+
function userFacingError(error) {
|
|
353
|
+
if (error instanceof Error)
|
|
354
|
+
return error.message;
|
|
355
|
+
return String(error);
|
|
356
|
+
}
|
|
357
|
+
function sameTerminalState(a, b) {
|
|
358
|
+
return a.activityCursor.line === b.activityCursor.line
|
|
359
|
+
&& a.activityCursor.column === b.activityCursor.column
|
|
360
|
+
&& samePositionOrNull(a.activitySelectionAnchor, b.activitySelectionAnchor)
|
|
361
|
+
&& a.cursor === b.cursor
|
|
362
|
+
&& a.draft === b.draft
|
|
363
|
+
&& a.focusedPanel === b.focusedPanel
|
|
364
|
+
&& a.mouseDragPanel === b.mouseDragPanel
|
|
365
|
+
&& a.selectionAnchor === b.selectionAnchor
|
|
366
|
+
&& a.transcriptCursor.line === b.transcriptCursor.line
|
|
367
|
+
&& a.transcriptCursor.column === b.transcriptCursor.column
|
|
368
|
+
&& a.transcriptOffset === b.transcriptOffset
|
|
369
|
+
&& samePositionOrNull(a.transcriptSelectionAnchor, b.transcriptSelectionAnchor);
|
|
370
|
+
}
|
|
371
|
+
function samePositionOrNull(a, b) {
|
|
372
|
+
if (a === null || b === null)
|
|
373
|
+
return a === b;
|
|
374
|
+
return a.line === b.line && a.column === b.column;
|
|
375
|
+
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { type WorkbenchRenderModel } from "@agent-api/app-engine/terminal";
|
|
2
|
+
import { type WorkbenchPanelPosition, type WorkbenchPanelSelection, type WorkbenchRenderModel } from "@agent-api/app-engine/terminal";
|
|
3
3
|
import { type AuthGateState } from "@agent-api/app-engine/workbench";
|
|
4
|
-
export declare function InkWorkbenchScreen({ renderModel, spinnerFrame, }: {
|
|
4
|
+
export declare function InkWorkbenchScreen({ activityCursor, activitySelection, focusedPanel, renderModel, spinnerFrame, transcriptCursor, transcriptSelection, }: {
|
|
5
|
+
activityCursor: WorkbenchPanelPosition;
|
|
6
|
+
activitySelection: WorkbenchPanelSelection | null;
|
|
7
|
+
focusedPanel: "activity" | "input" | "transcript";
|
|
5
8
|
renderModel: WorkbenchRenderModel;
|
|
6
9
|
spinnerFrame: number;
|
|
10
|
+
transcriptCursor: WorkbenchPanelPosition;
|
|
11
|
+
transcriptSelection: WorkbenchPanelSelection | null;
|
|
7
12
|
}): React.JSX.Element;
|
|
8
13
|
export declare function InkAuthGate({ cursorVisible, state }: {
|
|
9
14
|
cursorVisible: boolean;
|
|
@@ -1,16 +1,85 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { activityColor, busySpinner, } from "@agent-api/app-engine/terminal";
|
|
4
4
|
import { authMethods, } from "@agent-api/app-engine/workbench";
|
|
5
|
-
export function InkWorkbenchScreen({ renderModel, spinnerFrame, }) {
|
|
6
|
-
const activity = (_jsxs(Box, { flexDirection: "column", width: renderModel.layout === "wide" ? "28%" : "100%", height: renderModel.activityHeight, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { bold: true, wrap: "truncate", children: "Activity" }), renderModel.visibleActivities.map((activity) =>
|
|
7
|
-
|
|
5
|
+
export function InkWorkbenchScreen({ activityCursor, activitySelection, focusedPanel, renderModel, spinnerFrame, transcriptCursor, transcriptSelection, }) {
|
|
6
|
+
const activity = (_jsxs(Box, { flexDirection: "column", width: renderModel.layout === "wide" ? "28%" : "100%", height: renderModel.activityHeight, borderStyle: "single", borderColor: focusedPanel === "activity" ? "cyan" : "gray", paddingX: 1, children: [_jsx(Text, { bold: true, wrap: "truncate", children: "Activity" }), renderModel.visibleActivities.map((activity, index) => {
|
|
7
|
+
const cursor = focusedPanel === "activity" && index === activityCursor.line;
|
|
8
|
+
const text = `${new Date(activity.timestamp).toLocaleTimeString()} ${activity.text}`;
|
|
9
|
+
return (_jsxs(Text, { color: activityColor(activity.level), wrap: "truncate", children: [cursor ? _jsx(Text, { color: "cyan", children: "\u203A " }) : _jsx(Text, { children: " " }), _jsx(SelectableText, { cursorColumn: cursor && !activitySelection ? activityCursor.column : null, selection: lineSelection(index, activitySelection), text: text || " " })] }, activity.id));
|
|
10
|
+
})] }));
|
|
11
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { contextEnabled: renderModel.header.contextEnabled, conversation: renderModel.header.conversation, conversationId: renderModel.header.conversationId, conversationPreviousResponseId: renderModel.header.conversationPreviousResponseId, conversationStatus: renderModel.header.conversationStatus, 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, { height: renderModel.viewportHeight, flexDirection: renderModel.layout === "wide" ? "row" : "column", children: [_jsxs(Box, { flexDirection: "column", width: renderModel.layout === "wide" ? "72%" : "100%", paddingRight: renderModel.layout === "wide" ? 1 : 0, children: [renderModel.transcript.visibleLines.map((line, index) => (_jsx(TranscriptText, { cursorColumn: focusedPanel === "transcript" && renderModel.transcript.startLine + index - 1 === transcriptCursor.line && !transcriptSelection
|
|
12
|
+
? transcriptCursor.column
|
|
13
|
+
: null, line: line, lineSelection: lineSelection(renderModel.transcript.startLine + index - 1, transcriptSelection), lineCursor: focusedPanel === "transcript" && renderModel.transcript.startLine + index - 1 === transcriptCursor.line }, line.id))), renderModel.transcript.visibleLines.length === 0 && _jsx(Text, { color: "gray", children: "No transcript lines." })] }), activity] }), _jsxs(Box, { borderStyle: "single", borderColor: focusedPanel === "input" ? renderModel.input.busy ? "yellow" : "green" : "gray", paddingX: 1, flexDirection: "column", children: [_jsxs(Box, { children: [renderModel.input.fullAccess && (_jsx(Text, { color: "red", bold: true, inverse: true, children: "FULL ACCESS" })), renderModel.input.fullAccess && _jsx(Text, { children: " " }), _jsx(Text, { color: renderModel.input.busy ? "yellow" : "green", children: renderModel.input.label }), renderModel.input.statusText && (_jsxs(Text, { color: "yellow", children: [" ", busySpinner(spinnerFrame), " ", renderModel.input.statusText] }))] }), _jsx(Box, { flexDirection: "column", children: renderModel.input.lines.map((line, index) => (_jsx(Text, { wrap: "truncate", children: line.spans.map((span, spanIndex) => (_jsx(Text, { inverse: span.inverse, children: span.text }, spanIndex))) }, index))) })] }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { color: "gray", wrap: "truncate", children: renderModel.footerText }) })] }));
|
|
8
14
|
}
|
|
9
|
-
function TranscriptText({ line }) {
|
|
15
|
+
function TranscriptText({ cursorColumn, line, lineCursor, lineSelection, }) {
|
|
16
|
+
const anchor = lineCursor ? _jsx(Text, { color: "cyan", children: "\u203A " }) : line.anchor ? _jsx(Text, { color: "cyan", children: "\u25B8 " }) : _jsx(Text, { children: " " });
|
|
10
17
|
if (!line.spans || line.spans.length === 0) {
|
|
11
|
-
return (
|
|
18
|
+
return (_jsxs(Text, { bold: line.bold || lineCursor, color: line.color, inverse: line.inverse, wrap: "truncate", children: [anchor, _jsx(SelectableText, { cursorColumn: cursorColumn, selection: lineSelection, text: line.text || " " })] }));
|
|
12
19
|
}
|
|
13
|
-
return (
|
|
20
|
+
return (_jsxs(Text, { bold: line.bold || lineCursor, color: line.color, inverse: line.inverse, wrap: "truncate", children: [anchor, line.spans.map((span, index) => {
|
|
21
|
+
const offset = line.spans?.slice(0, index).reduce((sum, item) => sum + item.text.length, 0) ?? 0;
|
|
22
|
+
return (_jsx(SelectableText, { bold: span.bold, color: span.color, cursorColumn: cursorColumn, lineLength: line.text.length, offset: offset, selection: lineSelection, text: span.text }, index));
|
|
23
|
+
})] }));
|
|
24
|
+
}
|
|
25
|
+
function SelectableText({ bold, color, cursorColumn, lineLength, offset = 0, selection, text, }) {
|
|
26
|
+
const pieces = selectablePieces(text, { cursorColumn, lineLength: lineLength ?? text.length, offset, selection });
|
|
27
|
+
return (_jsx(_Fragment, { children: pieces.map((piece, index) => (_jsx(Text, { bold: bold, color: color, inverse: piece.inverse, children: piece.text }, index))) }));
|
|
28
|
+
}
|
|
29
|
+
function lineSelection(line, selection) {
|
|
30
|
+
if (!selection || line < selection.start.line || line > selection.end.line)
|
|
31
|
+
return null;
|
|
32
|
+
if (selection.start.line === selection.end.line) {
|
|
33
|
+
return selection.start.column === selection.end.column
|
|
34
|
+
? null
|
|
35
|
+
: { start: selection.start.column, end: selection.end.column };
|
|
36
|
+
}
|
|
37
|
+
if (line === selection.start.line)
|
|
38
|
+
return { start: selection.start.column, end: Number.POSITIVE_INFINITY };
|
|
39
|
+
if (line === selection.end.line)
|
|
40
|
+
return { start: 0, end: selection.end.column };
|
|
41
|
+
return { start: 0, end: Number.POSITIVE_INFINITY };
|
|
42
|
+
}
|
|
43
|
+
function selectablePieces(text, options) {
|
|
44
|
+
const textLength = text.length;
|
|
45
|
+
const selection = options.selection
|
|
46
|
+
? {
|
|
47
|
+
start: clamp(options.selection.start - options.offset, 0, textLength),
|
|
48
|
+
end: clamp(options.selection.end - options.offset, 0, textLength),
|
|
49
|
+
}
|
|
50
|
+
: null;
|
|
51
|
+
const segmentEnd = options.offset + textLength;
|
|
52
|
+
const cursorInSegment = options.cursorColumn != null
|
|
53
|
+
&& options.cursorColumn >= options.offset
|
|
54
|
+
&& (options.cursorColumn < segmentEnd
|
|
55
|
+
|| (options.cursorColumn === options.lineLength && segmentEnd === options.lineLength));
|
|
56
|
+
const cursor = cursorInSegment ? clamp((options.cursorColumn ?? 0) - options.offset, 0, textLength) : null;
|
|
57
|
+
const boundaries = new Set([0, textLength]);
|
|
58
|
+
if (selection && selection.start !== selection.end) {
|
|
59
|
+
boundaries.add(selection.start);
|
|
60
|
+
boundaries.add(selection.end);
|
|
61
|
+
}
|
|
62
|
+
if (cursor !== null) {
|
|
63
|
+
boundaries.add(cursor);
|
|
64
|
+
boundaries.add(Math.min(textLength, cursor + 1));
|
|
65
|
+
}
|
|
66
|
+
const sorted = Array.from(boundaries).sort((a, b) => a - b);
|
|
67
|
+
const pieces = [];
|
|
68
|
+
for (let index = 0; index < sorted.length - 1; index += 1) {
|
|
69
|
+
const start = sorted[index] ?? 0;
|
|
70
|
+
const end = sorted[index + 1] ?? start;
|
|
71
|
+
if (end <= start)
|
|
72
|
+
continue;
|
|
73
|
+
const selected = Boolean(selection && start >= selection.start && end <= selection.end);
|
|
74
|
+
const cursorAtPiece = cursor !== null && start >= cursor && start < cursor + 1;
|
|
75
|
+
pieces.push({ text: text.slice(start, end), inverse: selected || cursorAtPiece });
|
|
76
|
+
}
|
|
77
|
+
if (cursor === textLength)
|
|
78
|
+
pieces.push({ text: " ", inverse: true });
|
|
79
|
+
return pieces.length ? pieces : [{ text }];
|
|
80
|
+
}
|
|
81
|
+
function clamp(value, min, max) {
|
|
82
|
+
return Math.max(min, Math.min(value, max));
|
|
14
83
|
}
|
|
15
84
|
export function InkAuthGate({ cursorVisible, state }) {
|
|
16
85
|
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, { cursorVisible: cursorVisible, label: "Profile", value: state.profile }), state.status === "api_base_url" && _jsx(AuthPrompt, { cursorVisible: cursorVisible, label: "Base URL", value: state.baseURL }), state.status === "api_key" && _jsx(AuthPrompt, { cursorVisible: cursorVisible, label: "API key", value: state.apiKey ? "•".repeat(Math.min(state.apiKey.length, 32)) : "" }), state.status === "browser_profile" && _jsx(AuthPrompt, { cursorVisible: cursorVisible, label: "Profile", value: state.profile }), state.status === "browser_base_url" && _jsx(AuthPrompt, { cursorVisible: cursorVisible, 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..." })] }))] })] }));
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { WorkbenchTerminalMouseEvent } from "@agent-api/app-engine/terminal";
|
|
2
|
+
export declare const enableMouseReporting = "\u001B[?1000h\u001B[?1002h\u001B[?1006h";
|
|
3
|
+
export declare const disableMouseReporting = "\u001B[?1000l\u001B[?1002l\u001B[?1006l";
|
|
4
|
+
export declare function parseMouseEvent(input: string): WorkbenchTerminalMouseEvent | null;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const enableMouseReporting = "\x1b[?1000h\x1b[?1002h\x1b[?1006h";
|
|
2
|
+
export const disableMouseReporting = "\x1b[?1000l\x1b[?1002l\x1b[?1006l";
|
|
3
|
+
export function parseMouseEvent(input) {
|
|
4
|
+
const match = /\x1b\[<(\d+);(\d+);(\d+)([mM])/.exec(input);
|
|
5
|
+
if (!match)
|
|
6
|
+
return null;
|
|
7
|
+
const code = Number(match[1]);
|
|
8
|
+
const column = Number(match[2]);
|
|
9
|
+
const row = Number(match[3]);
|
|
10
|
+
if (!Number.isFinite(code) || !Number.isFinite(column) || !Number.isFinite(row))
|
|
11
|
+
return null;
|
|
12
|
+
if (code === 64)
|
|
13
|
+
return { button: "wheel_up", column, kind: "wheel", row };
|
|
14
|
+
if (code === 65)
|
|
15
|
+
return { button: "wheel_down", column, kind: "wheel", row };
|
|
16
|
+
if ((code & 32) === 32) {
|
|
17
|
+
return {
|
|
18
|
+
button: (code & 3) === 0 ? "left" : "unknown",
|
|
19
|
+
column,
|
|
20
|
+
kind: "motion",
|
|
21
|
+
row,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
button: (code & 3) === 0 ? "left" : "unknown",
|
|
26
|
+
column,
|
|
27
|
+
kind: match[4] === "m" ? "release" : "press",
|
|
28
|
+
row,
|
|
29
|
+
};
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-api/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.22",
|
|
4
4
|
"description": "First-class command line interface for Agent API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/scalebox-dev/agent-tui#readme",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"test": "npm run sync-version && npm run build && npm run smoke -w @agent-api/app-engine && node --test test/*.test.mjs"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@agent-api/app-engine": "^0.1.
|
|
38
|
+
"@agent-api/app-engine": "^0.1.21",
|
|
39
39
|
"commander": "^14.0.3",
|
|
40
40
|
"ink": "^6.8.0",
|
|
41
41
|
"react": "^19.2.7"
|