@agent-api/cli 0.0.3 → 0.1.1

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 +311 -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 +378 -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 +30 -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,291 @@
1
+ import { createInitialWorkbenchState, formatTranscript, formatTranscriptPreview, helpText, parsePendingApprovalCommand, parseWorkbenchCommand, workbenchReducer, workdirText, } from "../tui/workbench.js";
2
+ export function createWorkbenchEngine(options) {
3
+ let state = createInitialWorkbenchState(options);
4
+ let pendingApprovalInvalidInputs = 0;
5
+ const listeners = new Set();
6
+ const notify = () => {
7
+ for (const listener of listeners)
8
+ listener();
9
+ };
10
+ const dispatch = (action) => {
11
+ if (action.type === "local_tool.pending.set" || action.type === "local_tool.pending.clear") {
12
+ pendingApprovalInvalidInputs = 0;
13
+ }
14
+ const next = workbenchReducer(state, action);
15
+ if (Object.is(next, state))
16
+ return;
17
+ state = next;
18
+ notify();
19
+ };
20
+ return {
21
+ snapshot() {
22
+ return state;
23
+ },
24
+ subscribe(listener) {
25
+ listeners.add(listener);
26
+ return () => {
27
+ listeners.delete(listener);
28
+ };
29
+ },
30
+ dispatch,
31
+ handleCommand(command) {
32
+ switch (command.kind) {
33
+ case "quit":
34
+ return handled({ type: "exit" });
35
+ case "login":
36
+ return handled({ type: "login" });
37
+ case "logout":
38
+ return handled({ type: "logout" });
39
+ case "delete_profile":
40
+ return handled({ type: "delete_profile" });
41
+ case "switch_profile":
42
+ return handled({ type: "switch_profile", name: command.name });
43
+ case "auth_status":
44
+ return handled({ type: "show_auth_status" });
45
+ case "export":
46
+ return handled({
47
+ type: "export_transcript",
48
+ path: command.path,
49
+ transcript: formatTranscript(state.messages),
50
+ conversation: state.currentConversation,
51
+ });
52
+ case "refresh_catalog":
53
+ dispatch({ type: "activity.add", level: "success", text: "Preset and tool catalogs refreshed" });
54
+ dispatch({ type: "message.add", role: "system", text: "Cleared cached preset and server tool catalogs. The next agent turn will fetch fresh platform configuration." });
55
+ return handled({ type: "clear_preset_tool_catalog_cache" });
56
+ case "invalid":
57
+ dispatch({
58
+ type: "message.add",
59
+ role: "system",
60
+ text: `Unknown command: /${command.command}\nType /help for supported commands.`,
61
+ });
62
+ dispatch({ type: "activity.add", level: "warning", text: `Unknown command: /${command.command}` });
63
+ return handled();
64
+ case "help":
65
+ dispatch({ type: "message.add", role: "system", text: helpText() });
66
+ return handled();
67
+ case "clear":
68
+ dispatch({ type: "messages.clear" });
69
+ return handled();
70
+ case "render":
71
+ if (!command.mode) {
72
+ dispatch({ type: "message.add", role: "system", text: `Render mode: ${state.renderMode}. Use /render markdown or /render raw.` });
73
+ return handled();
74
+ }
75
+ dispatch({ type: "settings.set", settings: { renderMode: command.mode } });
76
+ dispatch({ type: "activity.add", level: "success", text: `Render mode: ${command.mode}` });
77
+ dispatch({ type: "message.add", role: "system", text: `Render mode set to ${command.mode}.` });
78
+ return handled();
79
+ case "transcript":
80
+ dispatch({ type: "message.add", role: "system", text: formatTranscriptPreview(state.messages) });
81
+ dispatch({ type: "activity.add", level: "success", text: "Transcript preview ready" });
82
+ return handled();
83
+ case "context":
84
+ dispatch({ type: "context.set", enabled: command.enabled ?? !state.contextEnabled });
85
+ return handled();
86
+ case "access":
87
+ if (!command.mode) {
88
+ dispatch({ type: "message.add", role: "system", text: `Local access: ${state.accessMode}. Use /access off, /access approval, or /access full.` });
89
+ return handled();
90
+ }
91
+ dispatch({ type: "access.set", mode: command.mode });
92
+ return handled();
93
+ case "model":
94
+ if (!command.value) {
95
+ dispatch({ type: "message.add", role: "system", text: `Model: ${state.runModel || "auto"}. Use /model <name> or /model auto.` });
96
+ return handled();
97
+ }
98
+ dispatch({ type: "settings.set", settings: { runModel: normalizeOptionalSetting(command.value, ["auto", "none", "off", "clear"]) } });
99
+ dispatch({ type: "activity.add", text: `Model: ${normalizeOptionalSetting(command.value, ["auto", "none", "off", "clear"]) || "auto"}` });
100
+ return handled();
101
+ case "workdir":
102
+ if (command.enabled === undefined) {
103
+ dispatch({
104
+ type: "message.add",
105
+ role: "system",
106
+ text: [
107
+ workdirText(state.workdir),
108
+ "",
109
+ `local_workdir tool: ${state.contextEnabled ? "on" : "off"}`,
110
+ `local_shell tool: ${state.contextEnabled ? "on" : "off"}`,
111
+ "Use /access approval or /access full to expose local tools, or /access off to hide them.",
112
+ ].join("\n"),
113
+ });
114
+ return handled();
115
+ }
116
+ dispatch({ type: "context.set", enabled: command.enabled });
117
+ dispatch({
118
+ type: "activity.add",
119
+ level: command.enabled ? "success" : "warning",
120
+ text: `local tools ${command.enabled ? "enabled" : "disabled"}`,
121
+ });
122
+ dispatch({
123
+ type: "message.add",
124
+ role: "system",
125
+ text: command.enabled
126
+ ? "local_workdir and local_shell are now available to the model in approval mode. Use /access full to allow execution without prompts."
127
+ : "local tools are now hidden from the model.",
128
+ });
129
+ return handled();
130
+ default:
131
+ return unhandled();
132
+ }
133
+ },
134
+ handleAgentEvent(event) {
135
+ switch (event.type) {
136
+ case "text.delta":
137
+ return event.delta ? eventResult({ type: "append_text_delta", delta: event.delta }) : eventResult();
138
+ case "response.started":
139
+ if (event.responseID) {
140
+ dispatch({ type: "activity.add", text: `Response started: ${event.responseID}` });
141
+ return eventResult({ type: "set_active_response_id", responseID: event.responseID });
142
+ }
143
+ dispatch({ type: "activity.add", text: "Response started" });
144
+ return eventResult();
145
+ case "response.completed":
146
+ dispatch({ type: "activity.add", level: "success", text: event.responseID ? `Response completed: ${event.responseID}` : "Response completed" });
147
+ return eventResult({ type: "flush_text_delta_buffer" });
148
+ case "response.failed":
149
+ dispatch({ type: "activity.add", level: "error", text: event.message });
150
+ return eventResult({ type: "flush_text_delta_buffer" });
151
+ case "reasoning.started":
152
+ dispatch({ type: "activity.add", text: "Reasoning started" });
153
+ return eventResult();
154
+ case "reasoning.stopped":
155
+ dispatch({ type: "activity.add", text: event.thought ? `Reasoning stopped: ${event.thought}` : "Reasoning stopped" });
156
+ return eventResult();
157
+ case "reasoning.search_queries":
158
+ dispatch({ type: "activity.add", text: `Search queries: ${event.queries.join(", ") || "none"}` });
159
+ return eventResult();
160
+ case "reasoning.search_results":
161
+ dispatch({ type: "activity.add", text: `Search results: ${event.count}` });
162
+ return eventResult();
163
+ case "reasoning.fetch_url_queries":
164
+ dispatch({ type: "activity.add", text: `Fetch URLs: ${event.urls.join(", ") || "none"}` });
165
+ return eventResult();
166
+ case "reasoning.fetch_url_results":
167
+ dispatch({ type: "activity.add", text: `Fetched URL results: ${event.count}` });
168
+ return eventResult();
169
+ case "tool.completed":
170
+ dispatch({ type: "activity.add", level: event.status === "failed" ? "error" : "success", text: `Tool completed: ${event.name}${event.status ? ` (${event.status})` : ""}` });
171
+ return eventResult();
172
+ case "local_tool.completed":
173
+ dispatch({
174
+ type: "activity.add",
175
+ level: event.requiresApproval ? "warning" : "success",
176
+ text: `Local tool: ${event.name}${event.action ? `.${event.action}` : ""}${event.requiresApproval ? " (approval required)" : ""}`,
177
+ });
178
+ return eventResult();
179
+ case "local_tool.approval_requested":
180
+ dispatch({
181
+ type: "local_tool.pending.set",
182
+ approval: {
183
+ name: event.name,
184
+ action: event.action,
185
+ arguments: event.arguments,
186
+ preview: event.preview,
187
+ callID: event.callID,
188
+ responseID: event.responseID,
189
+ },
190
+ });
191
+ dispatch({ type: "message.add", role: "system", text: formatLocalToolApproval(event) });
192
+ return eventResult();
193
+ case "model.requested":
194
+ dispatch({ type: "activity.add", text: `Model requested: ${modelLabel(event.model, event.provider)}` });
195
+ return eventResult();
196
+ case "model.completed":
197
+ dispatch({ type: "activity.add", level: "success", text: `Model completed: ${modelLabel(event.model, event.provider)}` });
198
+ return eventResult();
199
+ case "model.failed":
200
+ dispatch({ type: "activity.add", level: "error", text: `Model failed: ${modelLabel(event.model, event.provider)}` });
201
+ return eventResult();
202
+ case "step.completed":
203
+ dispatch({ type: "activity.add", level: "success", text: `Step completed: ${event.stepType || "step"}` });
204
+ return eventResult();
205
+ case "step.failed":
206
+ dispatch({ type: "activity.add", level: "error", text: `Step failed: ${event.stepType || "step"}` });
207
+ return eventResult();
208
+ case "raw":
209
+ return eventResult();
210
+ }
211
+ },
212
+ submit(input) {
213
+ const trimmed = input.trim();
214
+ if (!trimmed)
215
+ return { kind: "handled" };
216
+ if (state.pendingLocalTool) {
217
+ const command = parsePendingApprovalCommand(trimmed);
218
+ if (command) {
219
+ pendingApprovalInvalidInputs = 0;
220
+ return { kind: "command", command };
221
+ }
222
+ handleInvalidPendingApprovalInput();
223
+ return { kind: "handled" };
224
+ }
225
+ const command = parseWorkbenchCommand(trimmed);
226
+ if (command)
227
+ return { kind: "command", command };
228
+ return { kind: "prompt", prompt: trimmed };
229
+ },
230
+ };
231
+ function handleInvalidPendingApprovalInput() {
232
+ pendingApprovalInvalidInputs += 1;
233
+ const attempts = pendingApprovalInvalidInputs;
234
+ const maxAttempts = 3;
235
+ if (attempts >= maxAttempts) {
236
+ dispatch({
237
+ type: "message.add",
238
+ role: "system",
239
+ text: "Local approval aborted after too many invalid inputs. The pending action was not executed.",
240
+ });
241
+ dispatch({ type: "activity.add", level: "warning", text: "Local approval aborted" });
242
+ dispatch({ type: "local_tool.pending.clear" });
243
+ pendingApprovalInvalidInputs = 0;
244
+ return;
245
+ }
246
+ dispatch({
247
+ type: "message.add",
248
+ role: "system",
249
+ text: `Local approval is pending. Enter /apply or /yes to execute once, /apply-all or /yes-all to allow future local actions, or /reject or /no to discard. Invalid input ${attempts}/${maxAttempts}.`,
250
+ });
251
+ dispatch({ type: "activity.add", level: "warning", text: "Waiting for local approval command" });
252
+ }
253
+ }
254
+ function handled(...effects) {
255
+ return { handled: true, effects };
256
+ }
257
+ function unhandled() {
258
+ return { handled: false, effects: [] };
259
+ }
260
+ function eventResult(...effects) {
261
+ return { effects };
262
+ }
263
+ function modelLabel(model, provider) {
264
+ if (model && provider)
265
+ return `${provider}/${model}`;
266
+ return model || provider || "unknown";
267
+ }
268
+ function formatLocalToolApproval(event) {
269
+ const label = `${event.name}${event.action ? `.${event.action}` : ""}`;
270
+ return [
271
+ `Local action requires approval: ${label}.`,
272
+ event.preview ? `Preview:\n${formatPreview(event.preview)}` : undefined,
273
+ "Review it in the workbench, then use /apply to execute once, /apply-all to allow future local actions, or /reject to discard it.",
274
+ ].filter(Boolean).join("\n\n");
275
+ }
276
+ function formatPreview(preview) {
277
+ if (typeof preview === "string")
278
+ return preview;
279
+ try {
280
+ return JSON.stringify(preview, null, 2);
281
+ }
282
+ catch {
283
+ return String(preview);
284
+ }
285
+ }
286
+ function normalizeOptionalSetting(value, clearValues) {
287
+ const trimmed = value.trim();
288
+ if (!trimmed)
289
+ return undefined;
290
+ return clearValues.includes(trimmed.toLowerCase()) ? undefined : trimmed;
291
+ }
@@ -0,0 +1,44 @@
1
+ export interface WorkbenchInputKey {
2
+ backspace?: boolean;
3
+ ctrl?: boolean;
4
+ delete?: boolean;
5
+ downArrow?: boolean;
6
+ end?: boolean;
7
+ escape?: boolean;
8
+ home?: boolean;
9
+ meta?: boolean;
10
+ pageDown?: boolean;
11
+ pageUp?: boolean;
12
+ return?: boolean;
13
+ upArrow?: boolean;
14
+ }
15
+ export type WorkbenchInputEffect = {
16
+ type: "exit";
17
+ } | {
18
+ type: "scroll";
19
+ delta: number;
20
+ } | {
21
+ type: "scroll_top";
22
+ } | {
23
+ type: "scroll_bottom";
24
+ } | {
25
+ type: "abort";
26
+ } | {
27
+ type: "submit";
28
+ input: string;
29
+ } | {
30
+ type: "ignored_busy";
31
+ };
32
+ export interface WorkbenchInputResult {
33
+ draft: string;
34
+ effects: WorkbenchInputEffect[];
35
+ }
36
+ export interface WorkbenchInputContext {
37
+ busy: boolean;
38
+ draft: string;
39
+ viewportHeight: number;
40
+ }
41
+ export interface WorkbenchInputController {
42
+ handle(input: string, key: WorkbenchInputKey, context: WorkbenchInputContext): WorkbenchInputResult;
43
+ }
44
+ export declare function createWorkbenchInputController(): WorkbenchInputController;
@@ -0,0 +1,71 @@
1
+ import { createInputHistory } from "../tui/workbench.js";
2
+ export function createWorkbenchInputController() {
3
+ const history = createInputHistory();
4
+ return {
5
+ handle(input, key, context) {
6
+ if (key.ctrl && input === "c")
7
+ return result(context.draft, { type: "exit" });
8
+ if (key.pageUp || (key.ctrl && input === "u")) {
9
+ return result(context.draft, { type: "scroll", delta: Math.max(1, Math.floor(context.viewportHeight / 2)) });
10
+ }
11
+ if (key.pageDown || (key.ctrl && input === "d")) {
12
+ return result(context.draft, { type: "scroll", delta: -Math.max(1, Math.floor(context.viewportHeight / 2)) });
13
+ }
14
+ if (key.home)
15
+ return result(context.draft, { type: "scroll_top" });
16
+ if (key.end)
17
+ return result(context.draft, { type: "scroll_bottom" });
18
+ if (key.upArrow)
19
+ return result(history.previous(context.draft));
20
+ if (key.downArrow)
21
+ return result(history.next(context.draft));
22
+ if (context.busy) {
23
+ return handleBusyInput(input, key, context.draft, history);
24
+ }
25
+ return handleReadyInput(input, key, context.draft, history);
26
+ },
27
+ };
28
+ }
29
+ function handleBusyInput(input, key, draft, history) {
30
+ if (key.escape)
31
+ return result(draft, { type: "abort" });
32
+ if (key.return) {
33
+ const command = draft.trim();
34
+ history.record(command);
35
+ if (command === "/abort" || command === "/cancel")
36
+ return result("", { type: "abort" });
37
+ if (command)
38
+ return result("", { type: "ignored_busy" });
39
+ return result("");
40
+ }
41
+ if (key.backspace || key.delete) {
42
+ history.reset();
43
+ return result(draft.slice(0, -1));
44
+ }
45
+ if (input && !key.ctrl && !key.meta) {
46
+ history.reset();
47
+ return result(draft + input);
48
+ }
49
+ return result(draft);
50
+ }
51
+ function handleReadyInput(input, key, draft, history) {
52
+ if (key.return) {
53
+ const prompt = draft.trim();
54
+ if (!prompt)
55
+ return result(draft);
56
+ history.record(prompt);
57
+ return result("", { type: "submit", input: prompt });
58
+ }
59
+ if (key.backspace || key.delete) {
60
+ history.reset();
61
+ return result(draft.slice(0, -1));
62
+ }
63
+ if (input && !key.ctrl && !key.meta) {
64
+ history.reset();
65
+ return result(draft + input);
66
+ }
67
+ return result(draft);
68
+ }
69
+ function result(draft, ...effects) {
70
+ return { draft, effects };
71
+ }
@@ -0,0 +1,30 @@
1
+ import type { WorkbenchAuthController } from "./auth-controller.js";
2
+ import type { WorkbenchAction, WorkbenchWorkdirStatus } from "../tui/workbench.js";
3
+ import { checkForUpdate, formatUpdateNotice, type UpdateCheckResult } from "../update.js";
4
+ export type WorkbenchLifecycleEffect = {
5
+ type: "dispatch";
6
+ action: WorkbenchAction;
7
+ } | {
8
+ type: "close";
9
+ delayMs: number;
10
+ };
11
+ export interface WorkbenchLifecycleController {
12
+ maybeCheckForUpdate(): Promise<WorkbenchLifecycleEffect[]>;
13
+ refreshAuth(profile?: string): Promise<WorkbenchLifecycleEffect[]>;
14
+ initialPrompt(input: {
15
+ busy: boolean;
16
+ promptParts: string[];
17
+ requiresWorkdir?: boolean;
18
+ workdir: WorkbenchWorkdirStatus | null;
19
+ }): string | undefined;
20
+ }
21
+ export interface WorkbenchLifecycleControllerOptions {
22
+ authController: WorkbenchAuthController;
23
+ checkForUpdateImpl?: typeof checkForUpdate;
24
+ formatUpdateNoticeImpl?: typeof formatUpdateNotice;
25
+ formatError?: (error: unknown) => string;
26
+ refreshWindowMs?: number;
27
+ updateCheckEnabled?: boolean;
28
+ }
29
+ export declare function createWorkbenchLifecycleController(options: WorkbenchLifecycleControllerOptions): WorkbenchLifecycleController;
30
+ export declare function updateNoticeEffects(result: UpdateCheckResult | null | undefined, formatNotice?: typeof formatUpdateNotice): WorkbenchLifecycleEffect[];
@@ -0,0 +1,75 @@
1
+ import { checkForUpdate, formatUpdateNotice } from "../update.js";
2
+ export function createWorkbenchLifecycleController(options) {
3
+ const checkForUpdateImpl = options.checkForUpdateImpl ?? checkForUpdate;
4
+ const formatUpdateNoticeImpl = options.formatUpdateNoticeImpl ?? formatUpdateNotice;
5
+ const formatError = options.formatError ?? userFacingError;
6
+ const refreshWindowMs = options.refreshWindowMs ?? 5 * 60_000;
7
+ const updateCheckEnabled = options.updateCheckEnabled ?? process.env.AGENT_TUI_UPDATE_CHECK !== "0";
8
+ let updateNoticeShown = false;
9
+ let authRefreshWarningShown = false;
10
+ let initialPromptSubmitted = false;
11
+ return {
12
+ async maybeCheckForUpdate() {
13
+ if (updateNoticeShown || !updateCheckEnabled)
14
+ return [];
15
+ updateNoticeShown = true;
16
+ try {
17
+ const result = await checkForUpdateImpl();
18
+ return updateNoticeEffects(result, formatUpdateNoticeImpl);
19
+ }
20
+ catch {
21
+ return [];
22
+ }
23
+ },
24
+ async refreshAuth(profile) {
25
+ try {
26
+ const result = await options.authController.refresh(profile, refreshWindowMs);
27
+ if (!result.refreshed)
28
+ return [];
29
+ authRefreshWarningShown = false;
30
+ return [
31
+ { type: "dispatch", action: { type: "activity.add", level: "success", text: "Auth session refreshed" } },
32
+ ];
33
+ }
34
+ catch (error) {
35
+ if (authRefreshWarningShown)
36
+ return [];
37
+ authRefreshWarningShown = true;
38
+ return [
39
+ {
40
+ type: "dispatch",
41
+ action: {
42
+ type: "message.add",
43
+ role: "system",
44
+ text: `${formatError(error)}\n\nClosing the workbench because authenticated agent conversations are unavailable.`,
45
+ },
46
+ },
47
+ { type: "dispatch", action: { type: "activity.add", level: "error", text: "Auth session needs login; closing" } },
48
+ { type: "close", delayMs: 1500 },
49
+ ];
50
+ }
51
+ },
52
+ initialPrompt(input) {
53
+ if (initialPromptSubmitted || input.busy || (input.requiresWorkdir && !input.workdir))
54
+ return undefined;
55
+ const prompt = input.promptParts.join(" ").trim();
56
+ if (!prompt)
57
+ return undefined;
58
+ initialPromptSubmitted = true;
59
+ return prompt;
60
+ },
61
+ };
62
+ }
63
+ export function updateNoticeEffects(result, formatNotice = formatUpdateNotice) {
64
+ if (!result?.updateAvailable)
65
+ return [];
66
+ return [
67
+ { type: "dispatch", action: { type: "activity.add", level: "warning", text: `Update available: ${result.latest}` } },
68
+ { type: "dispatch", action: { type: "message.add", role: "system", text: formatNotice(result) } },
69
+ ];
70
+ }
71
+ function userFacingError(error) {
72
+ if (error instanceof Error)
73
+ return error.message;
74
+ return String(error);
75
+ }
@@ -0,0 +1,19 @@
1
+ import { openWorkdir } from "../workdir/index.js";
2
+ import type { WorkbenchState, WorkbenchWorkdirStatus } from "../tui/workbench.js";
3
+ export interface WorkbenchLocalController {
4
+ load(path?: string): Promise<WorkbenchWorkdirStatus>;
5
+ isLoaded(): boolean;
6
+ summaryText(): Promise<string>;
7
+ searchText(query: string): Promise<{
8
+ text: string;
9
+ count: number;
10
+ }>;
11
+ approvalPreview(approval: LocalApprovalLike): string;
12
+ applyApproval(approval: LocalApprovalLike): Promise<string | Record<string, unknown>>;
13
+ }
14
+ export interface WorkbenchLocalControllerOptions {
15
+ openWorkdirImpl?: typeof openWorkdir;
16
+ }
17
+ type LocalApprovalLike = NonNullable<WorkbenchState["pendingLocalTool"]>;
18
+ export declare function createWorkbenchLocalController(options?: WorkbenchLocalControllerOptions): WorkbenchLocalController;
19
+ export {};
@@ -0,0 +1,89 @@
1
+ import { createLocalShellToolRegistry, createLocalWorkdirToolRegistry } from "@agent-api/sdk/local";
2
+ import { openWorkdir } from "../workdir/index.js";
3
+ export function createWorkbenchLocalController(options = {}) {
4
+ const openWorkdirImpl = options.openWorkdirImpl ?? openWorkdir;
5
+ let workdir = null;
6
+ return {
7
+ async load(path) {
8
+ const next = await openWorkdirImpl({ path });
9
+ const summary = await next.summarize();
10
+ workdir = next;
11
+ return {
12
+ root: next.root,
13
+ name: next.name,
14
+ fileCount: summary.file_count,
15
+ totalBytes: summary.total_bytes,
16
+ scanTruncated: summary.scan_truncated,
17
+ };
18
+ },
19
+ isLoaded() {
20
+ return Boolean(workdir);
21
+ },
22
+ async summaryText() {
23
+ const current = requireWorkdir();
24
+ const summary = await current.summarize();
25
+ const previews = summary.text_previews
26
+ .slice(0, 5)
27
+ .map((preview) => `- ${preview.path} (${formatBytes(preview.size)})`)
28
+ .join("\n");
29
+ return [
30
+ `Workdir summary for ${current.name}`,
31
+ `Files: ${summary.file_count}`,
32
+ `Size: ${formatBytes(summary.total_bytes)}`,
33
+ previews ? `Previews:\n${previews}` : "No text previews available.",
34
+ ].join("\n");
35
+ },
36
+ async searchText(query) {
37
+ const trimmed = query.trim();
38
+ if (!trimmed) {
39
+ return { text: "Usage: /search <query>", count: 0 };
40
+ }
41
+ const current = requireWorkdir();
42
+ const results = await current.workdir.grep({ pattern: trimmed, limit: 12 });
43
+ const matches = results.matches
44
+ .map((match) => `${match.path}:${match.line_number}: ${match.line.trim()}`)
45
+ .join("\n");
46
+ return {
47
+ text: matches || `No matches for "${trimmed}".`,
48
+ count: results.matches.length,
49
+ };
50
+ },
51
+ approvalPreview(approval) {
52
+ return formatLocalToolApproval(approval);
53
+ },
54
+ async applyApproval(approval) {
55
+ const current = requireWorkdir();
56
+ const workdirRegistry = createLocalWorkdirToolRegistry(current.workdir, { accessMode: "full" });
57
+ const shellRegistry = createLocalShellToolRegistry({ workdir: current.workdir, accessMode: "full" });
58
+ if (approval.name === workdirRegistry.toolName) {
59
+ return await workdirRegistry.execute(approval.name, approval.arguments);
60
+ }
61
+ if (approval.name === shellRegistry.toolName) {
62
+ return await shellRegistry.execute(approval.name, approval.arguments);
63
+ }
64
+ throw new Error(`no local handler registered for function ${approval.name}`);
65
+ },
66
+ };
67
+ function requireWorkdir() {
68
+ if (!workdir)
69
+ throw new Error("Workdir is still loading.");
70
+ return workdir;
71
+ }
72
+ }
73
+ function formatLocalToolApproval(approval) {
74
+ return [
75
+ `Local approval requested: ${approval.name}${approval.action ? `.${approval.action}` : ""}`,
76
+ "Arguments:",
77
+ JSON.stringify(approval.arguments, null, 2),
78
+ approval.preview ? ["Preview:", JSON.stringify(approval.preview, null, 2)].join("\n") : "",
79
+ "",
80
+ "Use /apply to execute this action once, /apply-all to allow future local actions, or /reject to discard it.",
81
+ ].filter(Boolean).join("\n");
82
+ }
83
+ function formatBytes(bytes) {
84
+ if (bytes < 1024)
85
+ return `${bytes} B`;
86
+ if (bytes < 1024 * 1024)
87
+ return `${(bytes / 1024).toFixed(1)} KB`;
88
+ return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
89
+ }
@@ -0,0 +1,46 @@
1
+ import type { RenderMode, WorkbenchState } from "../tui/workbench.js";
2
+ import { type TranscriptViewModel } from "./view-model.js";
3
+ export interface WorkbenchRendererViewport {
4
+ rows: number;
5
+ columns: number;
6
+ }
7
+ export interface WorkbenchRenderModel {
8
+ activityHeight: number;
9
+ footerText: string;
10
+ header: {
11
+ accessMode: string;
12
+ contextEnabled: boolean;
13
+ conversation: string;
14
+ model: string;
15
+ pendingLocalLabel: string;
16
+ preset: string;
17
+ profile: string;
18
+ renderMode: RenderMode;
19
+ workdir: string;
20
+ };
21
+ input: {
22
+ busy: boolean;
23
+ draft: string;
24
+ fullAccess: boolean;
25
+ label: string;
26
+ waitingText: string;
27
+ };
28
+ terminalColumns: number;
29
+ terminalRows: number;
30
+ transcript: TranscriptViewModel;
31
+ transcriptWidth: number;
32
+ viewportHeight: number;
33
+ visibleActivities: WorkbenchState["activities"];
34
+ }
35
+ export interface BuildWorkbenchRenderModelInput {
36
+ draft: string;
37
+ profileName: string;
38
+ spinnerFrame: number;
39
+ state: WorkbenchState;
40
+ transcriptOffset: number;
41
+ viewport: Partial<WorkbenchRendererViewport>;
42
+ workdirFallback: string;
43
+ }
44
+ export declare function buildWorkbenchRenderModel(input: BuildWorkbenchRenderModelInput): WorkbenchRenderModel;
45
+ export declare function pendingLocalLabel(state: WorkbenchState): string;
46
+ export declare function busySpinner(frame: number): string;