@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 CHANGED
@@ -1,4 +1,4 @@
1
1
  export declare const cliName = "agent-tui";
2
2
  export declare const cliAuthor = "AgentsWay";
3
- export declare const cliVersion = "0.4.21";
3
+ export declare const cliVersion = "0.4.22";
4
4
  export declare const legacyCliName = "agent-api-cli";
package/dist/runtime.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export const cliName = "agent-tui";
2
2
  export const cliAuthor = "AgentsWay";
3
- export const cliVersion = "0.4.21";
3
+ export const cliVersion = "0.4.22";
4
4
  export const legacyCliName = "agent-api-cli";
@@ -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
+ }
@@ -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, createWorkbenchInputController, } from "@agent-api/app-engine/terminal";
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
- return () => showTerminalCursor(stdout);
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 [draft, setDraft] = useState("");
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 inputControllerRef = useRef(null);
144
- if (!inputControllerRef.current) {
145
- inputControllerRef.current = createWorkbenchInputController();
156
+ const terminalControllerRef = useRef(null);
157
+ if (!terminalControllerRef.current) {
158
+ terminalControllerRef.current = createWorkbenchTerminalController();
146
159
  }
147
- const inputController = inputControllerRef.current;
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
- }), [cursor, draft, options.workdir, profileName, selectionAnchor, spinnerFrame, state, terminalSize.columns, terminalSize.rows, transcriptOffset]);
176
+ }), [options.workdir, profileName, spinnerFrame, state, terminalSize.columns, terminalSize.rows, terminalState.cursor, terminalState.draft, terminalState.selectionAnchor, terminalState.transcriptOffset]);
164
177
  useEffect(() => {
165
- setTranscriptOffset((offset) => Math.min(offset, renderModel.transcript.maxOffset));
166
- }, [renderModel.transcript.maxOffset]);
167
- function scrollTranscript(delta) {
168
- setTranscriptOffset((offset) => Math.max(0, Math.min(renderModel.transcript.maxOffset, offset + delta)));
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 scrollTranscriptToTop() {
171
- setTranscriptOffset(renderModel.transcript.maxOffset);
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 scrollTranscriptToBottom() {
174
- setTranscriptOffset(0);
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 result = inputController.handle(input, key, {
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
- cursor,
212
- draft,
213
- selectionAnchor,
214
- viewportColumns: renderModel.input.viewportColumns,
215
- viewportHeight: renderModel.viewportHeight,
267
+ renderModel,
216
268
  });
217
- if (result.draft !== draft)
218
- setDraft(result.draft);
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
- scrollTranscript(effect.delta);
277
+ setTerminalState((current) => normalizeTerminalState({
278
+ ...current,
279
+ transcriptOffset: current.transcriptOffset + effect.delta,
280
+ }, renderModel));
230
281
  break;
231
282
  case "scroll_top":
232
- scrollTranscriptToTop();
283
+ setTerminalState((current) => normalizeTerminalState({
284
+ ...current,
285
+ transcriptOffset: renderModel.transcript.maxOffset,
286
+ }, renderModel));
233
287
  break;
234
288
  case "scroll_bottom":
235
- scrollTranscriptToBottom();
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 agentEngine.submit(effect.input);
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) => (_jsxs(Text, { color: activityColor(activity.level), wrap: "truncate", children: [new Date(activity.timestamp).toLocaleTimeString(), " ", activity.text] }, activity.id)))] }));
7
- 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) => (_jsx(TranscriptText, { line: line }, line.id))), renderModel.transcript.visibleLines.length === 0 && _jsx(Text, { color: "gray", children: "No transcript lines." })] }), activity] }), _jsxs(Box, { borderStyle: "single", borderColor: renderModel.input.busy ? "yellow" : "green", 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 }) })] }));
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 (_jsx(Text, { bold: line.bold, color: line.color, inverse: line.inverse, wrap: "truncate", children: line.text || " " }));
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 (_jsx(Text, { bold: line.bold, color: line.color, inverse: line.inverse, wrap: "truncate", children: line.spans.map((span, index) => (_jsx(Text, { bold: span.bold, color: span.color, inverse: span.inverse, children: span.text }, index))) }));
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.21",
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.20",
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"