@robota-sdk/agent-cli 3.0.0-beta.54 → 3.0.0-beta.56
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/README.md +48 -17
- package/dist/node/bin.js +4 -2
- package/dist/node/chunk-2JAZDNYT.js +246 -0
- package/dist/node/chunk-H3NRW5FW.js +4372 -0
- package/dist/node/index.cjs +2572 -682
- package/dist/node/index.d.cts +55 -3
- package/dist/node/index.d.ts +55 -3
- package/dist/node/index.js +12 -1
- package/dist/node/subagents/child-process-subagent-worker.d.ts +2 -0
- package/dist/node/subagents/child-process-subagent-worker.js +123 -0
- package/package.json +17 -10
- package/dist/node/chunk-ASBCJDLC.js +0 -2648
|
@@ -0,0 +1,4372 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_PROVIDER_DEFINITIONS,
|
|
3
|
+
createProviderFromSettings,
|
|
4
|
+
findProviderDefinition,
|
|
5
|
+
formatSupportedProviderTypes,
|
|
6
|
+
isSubagentWorkerChildMessage,
|
|
7
|
+
readMergedProviderSettings,
|
|
8
|
+
readProviderSettings
|
|
9
|
+
} from "./chunk-2JAZDNYT.js";
|
|
10
|
+
|
|
11
|
+
// src/background/managed-shell-process-runner.ts
|
|
12
|
+
import { spawn } from "child_process";
|
|
13
|
+
import {
|
|
14
|
+
BackgroundTaskError
|
|
15
|
+
} from "@robota-sdk/agent-sdk";
|
|
16
|
+
var DEFAULT_OUTPUT_LIMIT_BYTES = 3e4;
|
|
17
|
+
var DEFAULT_KILL_GRACE_MS = 2e3;
|
|
18
|
+
var LOG_PAGE_SIZE = 200;
|
|
19
|
+
function createCapture(limitBytes) {
|
|
20
|
+
const chunks = [];
|
|
21
|
+
let capturedBytes = 0;
|
|
22
|
+
let truncated = false;
|
|
23
|
+
return {
|
|
24
|
+
appendOutput(text) {
|
|
25
|
+
if (truncated) return;
|
|
26
|
+
const remaining = limitBytes - capturedBytes;
|
|
27
|
+
const buffer = Buffer.from(text, "utf8");
|
|
28
|
+
if (buffer.byteLength <= remaining) {
|
|
29
|
+
chunks.push(text);
|
|
30
|
+
capturedBytes += buffer.byteLength;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
chunks.push(buffer.subarray(0, Math.max(remaining, 0)).toString("utf8"));
|
|
34
|
+
chunks.push("\n[output truncated]\n");
|
|
35
|
+
truncated = true;
|
|
36
|
+
},
|
|
37
|
+
getOutput() {
|
|
38
|
+
return chunks.join("");
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function appendLog(lines, source, text) {
|
|
43
|
+
for (const line of text.split(/\r?\n/)) {
|
|
44
|
+
if (line.length > 0) lines.push(`[${source}] ${line}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function resolveShell(request) {
|
|
48
|
+
return { command: request.shell ?? "sh", args: ["-c", request.command] };
|
|
49
|
+
}
|
|
50
|
+
function sendInput(child, input) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const onError = (error) => {
|
|
53
|
+
child.stdin.off("error", onError);
|
|
54
|
+
reject(error);
|
|
55
|
+
};
|
|
56
|
+
child.stdin.once("error", onError);
|
|
57
|
+
child.stdin.end(input, () => {
|
|
58
|
+
child.stdin.off("error", onError);
|
|
59
|
+
resolve();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function createManagedShellProcessRunner(options = {}) {
|
|
64
|
+
const killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
|
|
65
|
+
return {
|
|
66
|
+
kind: "process",
|
|
67
|
+
start(task) {
|
|
68
|
+
if (task.request.kind !== "process") {
|
|
69
|
+
throw new BackgroundTaskError("runner", `Invalid process task kind: ${task.request.kind}`);
|
|
70
|
+
}
|
|
71
|
+
return startProcessTask(task.taskId, task.request, killGraceMs);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function startProcessTask(taskId, request, killGraceMs) {
|
|
76
|
+
const shell = resolveShell(request);
|
|
77
|
+
const runtime = {
|
|
78
|
+
taskId,
|
|
79
|
+
request,
|
|
80
|
+
child: spawn(shell.command, shell.args, {
|
|
81
|
+
cwd: request.cwd,
|
|
82
|
+
env: { ...process.env, ...request.env ?? {} },
|
|
83
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
84
|
+
}),
|
|
85
|
+
logs: [],
|
|
86
|
+
capture: createCapture(request.outputLimitBytes ?? DEFAULT_OUTPUT_LIMIT_BYTES),
|
|
87
|
+
killGraceMs
|
|
88
|
+
};
|
|
89
|
+
const result = createProcessResult(runtime);
|
|
90
|
+
return createProcessHandle(runtime, result);
|
|
91
|
+
}
|
|
92
|
+
function createProcessResult(runtime) {
|
|
93
|
+
let settled = false;
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
const timeoutTimer = runtime.request.timeoutMs ? setTimeout(() => {
|
|
96
|
+
appendLog(runtime.logs, "system", `timed out after ${runtime.request.timeoutMs}ms`);
|
|
97
|
+
runtime.child.kill("SIGTERM");
|
|
98
|
+
rejectOnceLocal(new BackgroundTaskError("timeout", "Background process timed out"));
|
|
99
|
+
}, runtime.request.timeoutMs) : void 0;
|
|
100
|
+
function clearTimers() {
|
|
101
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
102
|
+
if (runtime.killTimer) clearTimeout(runtime.killTimer);
|
|
103
|
+
}
|
|
104
|
+
function resolveOnce(exitCode, signalCode) {
|
|
105
|
+
if (settled) return;
|
|
106
|
+
settled = true;
|
|
107
|
+
clearTimers();
|
|
108
|
+
resolve({
|
|
109
|
+
taskId: runtime.taskId,
|
|
110
|
+
kind: "process",
|
|
111
|
+
output: runtime.capture.getOutput(),
|
|
112
|
+
exitCode,
|
|
113
|
+
signalCode
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function rejectOnceLocal(error) {
|
|
117
|
+
if (settled) return;
|
|
118
|
+
settled = true;
|
|
119
|
+
clearTimers();
|
|
120
|
+
reject(error);
|
|
121
|
+
}
|
|
122
|
+
attachOutputListeners(runtime);
|
|
123
|
+
runtime.child.on("error", (error) => {
|
|
124
|
+
appendLog(runtime.logs, "system", error.message);
|
|
125
|
+
rejectOnceLocal(new BackgroundTaskError("process", error.message));
|
|
126
|
+
});
|
|
127
|
+
runtime.child.on("close", (code, signal) => {
|
|
128
|
+
resolveOnce(code ?? void 0, signal ?? void 0);
|
|
129
|
+
});
|
|
130
|
+
if (runtime.request.stdin) {
|
|
131
|
+
runtime.child.stdin.write(runtime.request.stdin);
|
|
132
|
+
runtime.child.stdin.end();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function createProcessHandle(runtime, result) {
|
|
137
|
+
return {
|
|
138
|
+
taskId: runtime.taskId,
|
|
139
|
+
...runtime.child.pid !== void 0 ? { pid: runtime.child.pid } : {},
|
|
140
|
+
result,
|
|
141
|
+
cancel: async (reason) => {
|
|
142
|
+
cancelProcess(runtime, reason);
|
|
143
|
+
},
|
|
144
|
+
send: async (input) => {
|
|
145
|
+
if (!input.stdin) return;
|
|
146
|
+
await sendInput(runtime.child, input.stdin);
|
|
147
|
+
},
|
|
148
|
+
readLog: (cursor) => Promise.resolve(readProcessLog(runtime, cursor))
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function attachOutputListeners(runtime) {
|
|
152
|
+
runtime.child.stdout.on("data", (chunk) => {
|
|
153
|
+
const text = chunk.toString("utf8");
|
|
154
|
+
runtime.capture.appendOutput(text);
|
|
155
|
+
appendLog(runtime.logs, "stdout", text);
|
|
156
|
+
});
|
|
157
|
+
runtime.child.stderr.on("data", (chunk) => {
|
|
158
|
+
const text = chunk.toString("utf8");
|
|
159
|
+
runtime.capture.appendOutput(text);
|
|
160
|
+
appendLog(runtime.logs, "stderr", text);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function cancelProcess(runtime, reason) {
|
|
164
|
+
appendLog(runtime.logs, "system", reason ? `cancel requested: ${reason}` : "cancel requested");
|
|
165
|
+
if (!runtime.child.killed) runtime.child.kill("SIGTERM");
|
|
166
|
+
runtime.killTimer = setTimeout(() => {
|
|
167
|
+
if (!runtime.child.killed) runtime.child.kill("SIGKILL");
|
|
168
|
+
}, runtime.killGraceMs);
|
|
169
|
+
}
|
|
170
|
+
function readProcessLog(runtime, cursor) {
|
|
171
|
+
const offset = cursor?.offset ?? 0;
|
|
172
|
+
const nextOffset = Math.min(offset + LOG_PAGE_SIZE, runtime.logs.length);
|
|
173
|
+
return {
|
|
174
|
+
taskId: runtime.taskId,
|
|
175
|
+
cursor,
|
|
176
|
+
nextCursor: nextOffset < runtime.logs.length ? { offset: nextOffset } : void 0,
|
|
177
|
+
lines: runtime.logs.slice(offset, nextOffset)
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/subagents/git-worktree-isolation-adapter.ts
|
|
182
|
+
import { execFileSync } from "child_process";
|
|
183
|
+
import { randomUUID } from "crypto";
|
|
184
|
+
import { mkdirSync } from "fs";
|
|
185
|
+
import { join } from "path";
|
|
186
|
+
import {
|
|
187
|
+
BackgroundTaskError as BackgroundTaskError2
|
|
188
|
+
} from "@robota-sdk/agent-sdk";
|
|
189
|
+
var WORKTREE_DIR = ".robota/worktrees";
|
|
190
|
+
var BRANCH_PREFIX = "robota";
|
|
191
|
+
var GIT_ENCODING = "utf8";
|
|
192
|
+
var SHORT_ID_LENGTH = 8;
|
|
193
|
+
function createGitWorktreeIsolationAdapter(options) {
|
|
194
|
+
return new GitWorktreeIsolationAdapter(options);
|
|
195
|
+
}
|
|
196
|
+
var GitWorktreeIsolationAdapter = class {
|
|
197
|
+
worktreeDir;
|
|
198
|
+
branchPrefix;
|
|
199
|
+
constructor(options = {}) {
|
|
200
|
+
this.worktreeDir = options.worktreeDir ?? WORKTREE_DIR;
|
|
201
|
+
this.branchPrefix = options.branchPrefix ?? BRANCH_PREFIX;
|
|
202
|
+
}
|
|
203
|
+
prepare(request) {
|
|
204
|
+
const repoRoot = runGit(request.cwd, ["rev-parse", "--show-toplevel"]).trim();
|
|
205
|
+
const shortId = randomUUID().slice(0, SHORT_ID_LENGTH);
|
|
206
|
+
const branchName = `${this.branchPrefix}/${request.jobId}-${shortId}`;
|
|
207
|
+
const worktreeRoot = join(repoRoot, this.worktreeDir);
|
|
208
|
+
const worktreePath = join(worktreeRoot, `${request.jobId}-${shortId}`);
|
|
209
|
+
mkdirSync(worktreeRoot, { recursive: true });
|
|
210
|
+
runGit(repoRoot, ["worktree", "add", "-b", branchName, worktreePath, "HEAD"]);
|
|
211
|
+
return { repoRoot, worktreePath, branchName };
|
|
212
|
+
}
|
|
213
|
+
isClean(worktree) {
|
|
214
|
+
return runGit(worktree.worktreePath, ["status", "--porcelain"]).trim().length === 0;
|
|
215
|
+
}
|
|
216
|
+
remove(worktree) {
|
|
217
|
+
runGit(worktree.repoRoot, ["worktree", "remove", "--force", worktree.worktreePath]);
|
|
218
|
+
runGit(worktree.repoRoot, ["branch", "-D", worktree.branchName]);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
function runGit(cwd, args) {
|
|
222
|
+
try {
|
|
223
|
+
return execFileSync("git", args, {
|
|
224
|
+
cwd,
|
|
225
|
+
encoding: GIT_ENCODING,
|
|
226
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
227
|
+
});
|
|
228
|
+
} catch (error) {
|
|
229
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
230
|
+
throw new BackgroundTaskError2("runner", `git ${args.join(" ")} failed: ${message}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/subagents/child-process-subagent-runner.ts
|
|
235
|
+
import { fork } from "child_process";
|
|
236
|
+
import { existsSync, readFileSync } from "fs";
|
|
237
|
+
import { dirname, join as join2 } from "path";
|
|
238
|
+
import {
|
|
239
|
+
BackgroundTaskError as BackgroundTaskError5,
|
|
240
|
+
getBuiltInAgent,
|
|
241
|
+
createWorktreeSubagentRunner
|
|
242
|
+
} from "@robota-sdk/agent-sdk";
|
|
243
|
+
|
|
244
|
+
// src/subagents/child-process-subagent-runner-result.ts
|
|
245
|
+
import {
|
|
246
|
+
BackgroundTaskError as BackgroundTaskError4
|
|
247
|
+
} from "@robota-sdk/agent-sdk";
|
|
248
|
+
|
|
249
|
+
// src/subagents/child-process-subagent-transport.ts
|
|
250
|
+
import {
|
|
251
|
+
BackgroundTaskError as BackgroundTaskError3
|
|
252
|
+
} from "@robota-sdk/agent-sdk";
|
|
253
|
+
function handleWorkerMessage(message, startWorker, resolveOnce, rejectOnce, emit) {
|
|
254
|
+
switch (message.type) {
|
|
255
|
+
case "ready":
|
|
256
|
+
startWorker();
|
|
257
|
+
break;
|
|
258
|
+
case "result":
|
|
259
|
+
resolveOnce(message.output);
|
|
260
|
+
break;
|
|
261
|
+
case "error":
|
|
262
|
+
rejectOnce(new BackgroundTaskError3("runner", message.message));
|
|
263
|
+
break;
|
|
264
|
+
case "cancelled":
|
|
265
|
+
rejectOnce(new BackgroundTaskError3("runner", message.reason ?? "Subagent worker cancelled"));
|
|
266
|
+
break;
|
|
267
|
+
case "text_delta":
|
|
268
|
+
emit?.({ type: "background_task_text_delta", delta: message.delta });
|
|
269
|
+
break;
|
|
270
|
+
case "tool_start":
|
|
271
|
+
emit?.({
|
|
272
|
+
type: "background_task_tool_start",
|
|
273
|
+
toolName: message.toolName,
|
|
274
|
+
firstArg: extractFirstArg(message.toolArgs)
|
|
275
|
+
});
|
|
276
|
+
break;
|
|
277
|
+
case "tool_end":
|
|
278
|
+
emit?.({
|
|
279
|
+
type: "background_task_tool_end",
|
|
280
|
+
toolName: message.toolName,
|
|
281
|
+
success: message.success
|
|
282
|
+
});
|
|
283
|
+
break;
|
|
284
|
+
default:
|
|
285
|
+
rejectOnce(new BackgroundTaskError3("runner", "Unhandled subagent worker message"));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function extractFirstArg(toolArgs) {
|
|
289
|
+
if (!toolArgs) return void 0;
|
|
290
|
+
const firstValue = Object.values(toolArgs)[0];
|
|
291
|
+
if (firstValue === void 0) return void 0;
|
|
292
|
+
return typeof firstValue === "object" ? JSON.stringify(firstValue) : String(firstValue);
|
|
293
|
+
}
|
|
294
|
+
function sendWorkerMessage(child, message) {
|
|
295
|
+
return new Promise((resolve, reject) => {
|
|
296
|
+
if (!child.connected) {
|
|
297
|
+
reject(new BackgroundTaskError3("crash", "Subagent worker IPC channel is closed"));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
child.send(message, (error) => {
|
|
301
|
+
if (error) {
|
|
302
|
+
reject(error);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
resolve();
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
async function cancelChildProcess(runtime, reason) {
|
|
310
|
+
if (runtime.child.connected) {
|
|
311
|
+
await sendWorkerMessage(runtime.child, { type: "cancel", reason }).catch(() => void 0);
|
|
312
|
+
}
|
|
313
|
+
runtime.killTimer = setTimeout(() => {
|
|
314
|
+
if (!runtime.child.killed) {
|
|
315
|
+
runtime.child.kill("SIGTERM");
|
|
316
|
+
}
|
|
317
|
+
}, runtime.killGraceMs);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/subagents/child-process-subagent-runner-result.ts
|
|
321
|
+
function createChildProcessSubagentResult(options) {
|
|
322
|
+
return new Promise((resolve, reject) => {
|
|
323
|
+
new ChildProcessSubagentResultController(options, resolve, reject).start();
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
var ChildProcessSubagentResultController = class {
|
|
327
|
+
constructor(options, resolve, reject) {
|
|
328
|
+
this.options = options;
|
|
329
|
+
this.resolve = resolve;
|
|
330
|
+
this.reject = reject;
|
|
331
|
+
this.timeoutTimer = createTimeoutTimer(this.options.runtime, (error) => this.rejectOnce(error));
|
|
332
|
+
}
|
|
333
|
+
settled = false;
|
|
334
|
+
started = false;
|
|
335
|
+
timeoutTimer;
|
|
336
|
+
start() {
|
|
337
|
+
const { child } = this.options.runtime;
|
|
338
|
+
child.on("message", this.onMessage);
|
|
339
|
+
child.on("error", this.onError);
|
|
340
|
+
child.on("exit", this.onExit);
|
|
341
|
+
child.once("spawn", () => {
|
|
342
|
+
setImmediate(this.startWorker);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
startWorker = () => {
|
|
346
|
+
if (this.started) return;
|
|
347
|
+
this.started = true;
|
|
348
|
+
const { child } = this.options.runtime;
|
|
349
|
+
void sendWorkerMessage(child, { type: "start", payload: this.options.payload }).catch(
|
|
350
|
+
(error) => {
|
|
351
|
+
this.rejectOnce(error instanceof Error ? error : new Error(String(error)));
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
};
|
|
355
|
+
onMessage = (message) => {
|
|
356
|
+
if (!isSubagentWorkerChildMessage(message)) {
|
|
357
|
+
this.rejectOnce(
|
|
358
|
+
new BackgroundTaskError4("runner", "Received malformed subagent worker message")
|
|
359
|
+
);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const { job } = this.options.runtime;
|
|
363
|
+
handleWorkerMessage(message, this.startWorker, this.resolveOnce, this.rejectOnce, job.emit);
|
|
364
|
+
};
|
|
365
|
+
onError = (error) => {
|
|
366
|
+
this.rejectOnce(new BackgroundTaskError4("crash", error.message));
|
|
367
|
+
};
|
|
368
|
+
onExit = (code, signal) => {
|
|
369
|
+
if (this.settled) return;
|
|
370
|
+
this.rejectOnce(new BackgroundTaskError4("crash", formatEarlyExitMessage(code, signal)));
|
|
371
|
+
};
|
|
372
|
+
resolveOnce = (output) => {
|
|
373
|
+
if (this.settled) return;
|
|
374
|
+
this.settled = true;
|
|
375
|
+
this.clearTimers();
|
|
376
|
+
this.cleanup();
|
|
377
|
+
const { runtime, resolveTranscriptPath } = this.options;
|
|
378
|
+
this.resolve(toSubagentResult(runtime.job, output, resolveTranscriptPath));
|
|
379
|
+
};
|
|
380
|
+
rejectOnce = (error) => {
|
|
381
|
+
if (this.settled) return;
|
|
382
|
+
this.settled = true;
|
|
383
|
+
this.clearTimers();
|
|
384
|
+
this.cleanup();
|
|
385
|
+
this.reject(error);
|
|
386
|
+
};
|
|
387
|
+
clearTimers() {
|
|
388
|
+
if (this.timeoutTimer) clearTimeout(this.timeoutTimer);
|
|
389
|
+
if (this.options.runtime.killTimer) clearTimeout(this.options.runtime.killTimer);
|
|
390
|
+
}
|
|
391
|
+
cleanup() {
|
|
392
|
+
const { child } = this.options.runtime;
|
|
393
|
+
child.off("message", this.onMessage);
|
|
394
|
+
child.off("error", this.onError);
|
|
395
|
+
child.off("exit", this.onExit);
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
function createCancellationResult(jobId) {
|
|
399
|
+
let settled = false;
|
|
400
|
+
let rejectFn = () => {
|
|
401
|
+
};
|
|
402
|
+
const promise = new Promise((_resolve, reject) => {
|
|
403
|
+
rejectFn = reject;
|
|
404
|
+
});
|
|
405
|
+
return {
|
|
406
|
+
promise,
|
|
407
|
+
reject(reason) {
|
|
408
|
+
if (settled) return;
|
|
409
|
+
settled = true;
|
|
410
|
+
rejectFn(new BackgroundTaskError4("runner", reason ?? `Subagent job cancelled: ${jobId}`));
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
function createTimeoutTimer(runtime, rejectOnce) {
|
|
415
|
+
if (!runtime.job.request.timeoutMs) return void 0;
|
|
416
|
+
return setTimeout(() => {
|
|
417
|
+
void cancelChildProcess(runtime, "Subagent worker timed out");
|
|
418
|
+
rejectOnce(new BackgroundTaskError4("timeout", "Subagent worker timed out"));
|
|
419
|
+
}, runtime.job.request.timeoutMs);
|
|
420
|
+
}
|
|
421
|
+
function toSubagentResult(job, output, resolveTranscriptPath) {
|
|
422
|
+
const transcriptPath = resolveTranscriptPath(job);
|
|
423
|
+
return {
|
|
424
|
+
jobId: job.jobId,
|
|
425
|
+
output,
|
|
426
|
+
...transcriptPath ? { metadata: { transcriptPath, logPath: transcriptPath } } : {}
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function formatEarlyExitMessage(code, signal) {
|
|
430
|
+
const detail = signal !== null ? `signal ${signal}` : `exit code ${code === null ? "unknown" : code}`;
|
|
431
|
+
return `Subagent worker exited before result: ${detail}`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// src/subagents/child-process-subagent-runner.ts
|
|
435
|
+
var DEFAULT_KILL_GRACE_MS2 = 2e3;
|
|
436
|
+
var LOG_PAGE_SIZE2 = 200;
|
|
437
|
+
function createChildProcessSubagentRunnerFactory(options = {}) {
|
|
438
|
+
return (deps) => {
|
|
439
|
+
const runner = new ChildProcessSubagentRunner(deps, options);
|
|
440
|
+
if (options.worktreeIsolation === false) return runner;
|
|
441
|
+
return createWorktreeSubagentRunner({
|
|
442
|
+
runner,
|
|
443
|
+
worktreeAdapter: options.worktreeAdapter ?? createGitWorktreeIsolationAdapter(),
|
|
444
|
+
hooks: deps.config.hooks,
|
|
445
|
+
hookTypeExecutors: deps.hookTypeExecutors
|
|
446
|
+
});
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
var ChildProcessSubagentRunner = class {
|
|
450
|
+
constructor(deps, options = {}) {
|
|
451
|
+
this.deps = deps;
|
|
452
|
+
this.workerPath = options.workerPath ?? resolveDefaultWorkerPath();
|
|
453
|
+
this.execArgv = options.execArgv;
|
|
454
|
+
this.killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS2;
|
|
455
|
+
this.providerConfig = options.providerConfig;
|
|
456
|
+
this.env = options.env;
|
|
457
|
+
this.logsDir = options.logsDir;
|
|
458
|
+
}
|
|
459
|
+
workerPath;
|
|
460
|
+
execArgv;
|
|
461
|
+
killGraceMs;
|
|
462
|
+
providerConfig;
|
|
463
|
+
env;
|
|
464
|
+
logsDir;
|
|
465
|
+
start(job) {
|
|
466
|
+
const child = fork(this.workerPath, [], {
|
|
467
|
+
cwd: job.request.cwd,
|
|
468
|
+
env: { ...process.env, ...this.env ?? {} },
|
|
469
|
+
execArgv: this.execArgv ?? resolveDefaultExecArgv(this.workerPath),
|
|
470
|
+
stdio: ["ignore", "ignore", "ignore", "ipc"]
|
|
471
|
+
});
|
|
472
|
+
const runtime = {
|
|
473
|
+
job,
|
|
474
|
+
child,
|
|
475
|
+
killGraceMs: this.killGraceMs
|
|
476
|
+
};
|
|
477
|
+
const payload = this.createStartPayload(job);
|
|
478
|
+
const workerResult = createChildProcessSubagentResult({
|
|
479
|
+
runtime,
|
|
480
|
+
payload,
|
|
481
|
+
resolveTranscriptPath: (request) => this.resolveTranscriptPath(request)
|
|
482
|
+
});
|
|
483
|
+
const cancellation = createCancellationResult(job.jobId);
|
|
484
|
+
void workerResult.catch(() => void 0);
|
|
485
|
+
const result = Promise.race([workerResult, cancellation.promise]);
|
|
486
|
+
const transcriptPath = this.resolveTranscriptPath(job);
|
|
487
|
+
return {
|
|
488
|
+
jobId: job.jobId,
|
|
489
|
+
...child.pid !== void 0 && { pid: child.pid },
|
|
490
|
+
...transcriptPath !== void 0 && { transcriptPath, logPath: transcriptPath },
|
|
491
|
+
result,
|
|
492
|
+
cancel: async (reason) => {
|
|
493
|
+
cancellation.reject(reason);
|
|
494
|
+
await cancelChildProcess(runtime, reason);
|
|
495
|
+
},
|
|
496
|
+
send: async (prompt) => {
|
|
497
|
+
await sendWorkerMessage(child, { type: "send", prompt });
|
|
498
|
+
},
|
|
499
|
+
...transcriptPath !== void 0 && {
|
|
500
|
+
readLog: async (cursor) => readTranscriptLog(job.jobId, transcriptPath, cursor)
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
createStartPayload(job) {
|
|
505
|
+
const definition = resolveAgentDefinition(job.request.type, this.deps.customAgentRegistry);
|
|
506
|
+
return {
|
|
507
|
+
jobId: job.jobId,
|
|
508
|
+
request: job.request,
|
|
509
|
+
agentDefinition: applyRequestOverrides(definition, job),
|
|
510
|
+
parentConfig: this.deps.config,
|
|
511
|
+
parentContext: this.deps.context,
|
|
512
|
+
providerProfile: createProviderProfile(this.providerConfig, this.deps, job),
|
|
513
|
+
permissionMode: this.deps.permissionMode,
|
|
514
|
+
...this.logsDir ? { logsDir: this.logsDir } : {}
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
resolveTranscriptPath(job) {
|
|
518
|
+
if (!this.logsDir) return void 0;
|
|
519
|
+
return join2(this.logsDir, job.request.parentSessionId, "subagents", `${job.jobId}.jsonl`);
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
function resolveAgentDefinition(agentType, customRegistry) {
|
|
523
|
+
const definition = customRegistry?.(agentType) ?? getBuiltInAgent(agentType);
|
|
524
|
+
if (!definition) {
|
|
525
|
+
throw new BackgroundTaskError5("validation", `Unknown agent type: ${agentType}`);
|
|
526
|
+
}
|
|
527
|
+
return definition;
|
|
528
|
+
}
|
|
529
|
+
function applyRequestOverrides(definition, job) {
|
|
530
|
+
return {
|
|
531
|
+
...definition,
|
|
532
|
+
...job.request.model ? { model: job.request.model } : {},
|
|
533
|
+
...job.request.allowedTools ? { tools: job.request.allowedTools } : {},
|
|
534
|
+
...job.request.disallowedTools ? { disallowedTools: job.request.disallowedTools } : {}
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
function createProviderProfile(providerConfig, deps, job) {
|
|
538
|
+
const provider = providerConfig ?? deps.config.provider;
|
|
539
|
+
return {
|
|
540
|
+
profileName: deps.config.currentProvider,
|
|
541
|
+
type: provider.name,
|
|
542
|
+
model: job.request.model ?? provider.model,
|
|
543
|
+
apiKey: provider.apiKey,
|
|
544
|
+
baseURL: provider.baseURL,
|
|
545
|
+
timeout: provider.timeout
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
function resolveDefaultWorkerPath() {
|
|
549
|
+
const entryPoint = process.argv[1] ?? "";
|
|
550
|
+
const entryDir = entryPoint ? dirname(entryPoint) : process.cwd();
|
|
551
|
+
const extension = entryPoint.endsWith(".ts") || entryPoint.endsWith(".tsx") ? ".ts" : ".js";
|
|
552
|
+
const candidates = [
|
|
553
|
+
join2(entryDir, "subagents", `child-process-subagent-worker${extension}`),
|
|
554
|
+
join2(entryDir, `child-process-subagent-worker${extension}`)
|
|
555
|
+
];
|
|
556
|
+
for (const candidate of candidates) {
|
|
557
|
+
if (existsSync(candidate)) {
|
|
558
|
+
return candidate;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return candidates[0];
|
|
562
|
+
}
|
|
563
|
+
function resolveDefaultExecArgv(workerPath) {
|
|
564
|
+
if (!workerPath.endsWith(".ts")) {
|
|
565
|
+
return process.execArgv;
|
|
566
|
+
}
|
|
567
|
+
if (process.execArgv.some((arg) => arg.includes("tsx"))) {
|
|
568
|
+
return process.execArgv;
|
|
569
|
+
}
|
|
570
|
+
return [...process.execArgv, "--import", "tsx"];
|
|
571
|
+
}
|
|
572
|
+
function readTranscriptLog(jobId, transcriptPath, cursor) {
|
|
573
|
+
const offset = cursor?.offset ?? 0;
|
|
574
|
+
if (!existsSync(transcriptPath)) {
|
|
575
|
+
return {
|
|
576
|
+
taskId: jobId,
|
|
577
|
+
cursor,
|
|
578
|
+
lines: []
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
const lines = readFileSync(transcriptPath, "utf8").split(/\r?\n/).filter(Boolean);
|
|
582
|
+
const nextOffset = Math.min(offset + LOG_PAGE_SIZE2, lines.length);
|
|
583
|
+
return {
|
|
584
|
+
taskId: jobId,
|
|
585
|
+
cursor,
|
|
586
|
+
nextCursor: nextOffset < lines.length ? { offset: nextOffset } : void 0,
|
|
587
|
+
lines: lines.slice(offset, nextOffset)
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/cli.ts
|
|
592
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
593
|
+
import { join as join7, dirname as dirname3 } from "path";
|
|
594
|
+
import { fileURLToPath } from "url";
|
|
595
|
+
import { InteractiveSession as InteractiveSession2, projectPaths } from "@robota-sdk/agent-sdk";
|
|
596
|
+
import { SessionStore } from "@robota-sdk/agent-sessions";
|
|
597
|
+
|
|
598
|
+
// src/utils/cli-args.ts
|
|
599
|
+
import { parseArgs } from "util";
|
|
600
|
+
var VALID_MODES = ["plan", "default", "acceptEdits", "bypassPermissions"];
|
|
601
|
+
function parsePermissionMode(raw) {
|
|
602
|
+
if (raw === void 0) return void 0;
|
|
603
|
+
if (!VALID_MODES.includes(raw)) {
|
|
604
|
+
process.stderr.write(`Invalid --permission-mode "${raw}". Valid: ${VALID_MODES.join(" | ")}
|
|
605
|
+
`);
|
|
606
|
+
process.exit(1);
|
|
607
|
+
}
|
|
608
|
+
return raw;
|
|
609
|
+
}
|
|
610
|
+
function parseMaxTurns(raw) {
|
|
611
|
+
if (raw === void 0) return void 0;
|
|
612
|
+
const n = parseInt(raw, 10);
|
|
613
|
+
if (isNaN(n) || n <= 0) {
|
|
614
|
+
process.stderr.write(`Invalid --max-turns "${raw}". Must be a positive integer.
|
|
615
|
+
`);
|
|
616
|
+
process.exit(1);
|
|
617
|
+
}
|
|
618
|
+
return n;
|
|
619
|
+
}
|
|
620
|
+
function parseCliArgs() {
|
|
621
|
+
const { values, positionals } = parseArgs({
|
|
622
|
+
allowPositionals: true,
|
|
623
|
+
options: {
|
|
624
|
+
p: { type: "boolean", short: "p", default: false },
|
|
625
|
+
continue: { type: "boolean", short: "c", default: false },
|
|
626
|
+
resume: { type: "string", short: "r" },
|
|
627
|
+
model: { type: "string" },
|
|
628
|
+
language: { type: "string" },
|
|
629
|
+
"permission-mode": { type: "string" },
|
|
630
|
+
"max-turns": { type: "string" },
|
|
631
|
+
"fork-session": { type: "boolean", default: false },
|
|
632
|
+
name: { type: "string", short: "n" },
|
|
633
|
+
"output-format": { type: "string" },
|
|
634
|
+
"system-prompt": { type: "string" },
|
|
635
|
+
"append-system-prompt": { type: "string" },
|
|
636
|
+
version: { type: "boolean", default: false },
|
|
637
|
+
reset: { type: "boolean", default: false },
|
|
638
|
+
bare: { type: "boolean", default: false },
|
|
639
|
+
"allowed-tools": { type: "string" },
|
|
640
|
+
"no-session-persistence": { type: "boolean", default: false },
|
|
641
|
+
"json-schema": { type: "string" },
|
|
642
|
+
configure: { type: "boolean", default: false },
|
|
643
|
+
"configure-provider": { type: "string" },
|
|
644
|
+
provider: { type: "string" },
|
|
645
|
+
type: { type: "string" },
|
|
646
|
+
"base-url": { type: "string" },
|
|
647
|
+
"api-key": { type: "string" },
|
|
648
|
+
"api-key-env": { type: "string" },
|
|
649
|
+
"set-current": { type: "boolean", default: false },
|
|
650
|
+
"settings-scope": { type: "string" }
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
return {
|
|
654
|
+
positional: positionals,
|
|
655
|
+
printMode: values["p"] ?? false,
|
|
656
|
+
continueMode: values["continue"] ?? false,
|
|
657
|
+
resumeId: values["resume"],
|
|
658
|
+
model: values["model"],
|
|
659
|
+
language: values["language"],
|
|
660
|
+
permissionMode: parsePermissionMode(values["permission-mode"]),
|
|
661
|
+
maxTurns: parseMaxTurns(values["max-turns"]),
|
|
662
|
+
forkSession: values["fork-session"] ?? false,
|
|
663
|
+
sessionName: values["name"],
|
|
664
|
+
outputFormat: values["output-format"],
|
|
665
|
+
systemPrompt: values["system-prompt"],
|
|
666
|
+
appendSystemPrompt: values["append-system-prompt"],
|
|
667
|
+
version: values["version"] ?? false,
|
|
668
|
+
reset: values["reset"] ?? false,
|
|
669
|
+
bare: values["bare"] ?? false,
|
|
670
|
+
allowedTools: values["allowed-tools"],
|
|
671
|
+
noSessionPersistence: values["no-session-persistence"] ?? false,
|
|
672
|
+
jsonSchema: values["json-schema"],
|
|
673
|
+
configure: values["configure"] ?? false,
|
|
674
|
+
configureProvider: values["configure-provider"],
|
|
675
|
+
provider: values["provider"],
|
|
676
|
+
providerType: values["type"],
|
|
677
|
+
baseURL: values["base-url"],
|
|
678
|
+
apiKey: values["api-key"],
|
|
679
|
+
apiKeyEnv: values["api-key-env"],
|
|
680
|
+
setCurrent: values["set-current"] ?? false,
|
|
681
|
+
settingsScope: values["settings-scope"]
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// src/utils/settings-io.ts
|
|
686
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync as mkdirSync2, unlinkSync } from "fs";
|
|
687
|
+
import { join as join3, dirname as dirname2 } from "path";
|
|
688
|
+
function getUserSettingsPath() {
|
|
689
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/";
|
|
690
|
+
return join3(home, ".robota", "settings.json");
|
|
691
|
+
}
|
|
692
|
+
function readSettings(path) {
|
|
693
|
+
if (!existsSync2(path)) return {};
|
|
694
|
+
const raw = readFileSync2(path, "utf8");
|
|
695
|
+
try {
|
|
696
|
+
return JSON.parse(raw);
|
|
697
|
+
} catch {
|
|
698
|
+
process.stderr.write(`Warning: corrupt settings file at ${path}, resetting to defaults
|
|
699
|
+
`);
|
|
700
|
+
return {};
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
function writeSettings(path, settings) {
|
|
704
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
705
|
+
writeFileSync(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
706
|
+
}
|
|
707
|
+
function updateModelInSettings(settingsPath, modelId) {
|
|
708
|
+
const settings = readSettings(settingsPath);
|
|
709
|
+
const currentProvider = settings.currentProvider;
|
|
710
|
+
const providers = settings.providers;
|
|
711
|
+
if (typeof currentProvider === "string" && isSettingsData(providers)) {
|
|
712
|
+
const providerMap = providers;
|
|
713
|
+
providerMap[currentProvider] = {
|
|
714
|
+
...isSettingsData(providerMap[currentProvider]) ? providerMap[currentProvider] : {},
|
|
715
|
+
model: modelId
|
|
716
|
+
};
|
|
717
|
+
settings.providers = providerMap;
|
|
718
|
+
} else {
|
|
719
|
+
settings.provider = {
|
|
720
|
+
...isSettingsData(settings.provider) ? settings.provider : {},
|
|
721
|
+
model: modelId
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
writeSettings(settingsPath, settings);
|
|
725
|
+
}
|
|
726
|
+
function isSettingsData(value) {
|
|
727
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
728
|
+
}
|
|
729
|
+
function deleteSettings(path) {
|
|
730
|
+
if (existsSync2(path)) {
|
|
731
|
+
unlinkSync(path);
|
|
732
|
+
return true;
|
|
733
|
+
}
|
|
734
|
+
return false;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// src/utils/provider-setup.ts
|
|
738
|
+
import { join as join4 } from "path";
|
|
739
|
+
import { homedir } from "os";
|
|
740
|
+
|
|
741
|
+
// src/utils/settings-check.ts
|
|
742
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
743
|
+
function checkSettingsFile(filePath, providerDefinitions = []) {
|
|
744
|
+
if (!existsSync3(filePath)) return "missing";
|
|
745
|
+
try {
|
|
746
|
+
const raw = readFileSync3(filePath, "utf8").trim();
|
|
747
|
+
if (raw.length === 0) return "incomplete";
|
|
748
|
+
const parsed = JSON.parse(raw);
|
|
749
|
+
if (!hasUsableProviderConfig(parsed, providerDefinitions)) return "incomplete";
|
|
750
|
+
return "valid";
|
|
751
|
+
} catch {
|
|
752
|
+
return "corrupt";
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
function hasUsableProviderConfig(settings, providerDefinitions) {
|
|
756
|
+
if (settings.provider && isUsableProviderProfile(settings.provider.name, settings.provider, providerDefinitions)) {
|
|
757
|
+
return true;
|
|
758
|
+
}
|
|
759
|
+
if (typeof settings.currentProvider !== "string") {
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
762
|
+
const profile = settings.providers?.[settings.currentProvider];
|
|
763
|
+
return isUsableProviderProfile(profile?.type, profile, providerDefinitions);
|
|
764
|
+
}
|
|
765
|
+
function isUsableProviderProfile(type, profile, providerDefinitions) {
|
|
766
|
+
if (!profile) {
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
if (profile.apiKey) {
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
if (!type) {
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
const definition = findProviderDefinition(providerDefinitions, type);
|
|
776
|
+
if (definition === void 0) {
|
|
777
|
+
return false;
|
|
778
|
+
}
|
|
779
|
+
return definition.requiresApiKey !== true || definition.defaults?.apiKey !== void 0;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// src/utils/provider-settings.ts
|
|
783
|
+
function upsertProviderProfile(settings, profileName, profile) {
|
|
784
|
+
return {
|
|
785
|
+
...settings,
|
|
786
|
+
providers: {
|
|
787
|
+
...settings.providers ?? {},
|
|
788
|
+
[profileName]: profile
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
function setCurrentProvider(settings, profileName) {
|
|
793
|
+
if (!settings.providers?.[profileName]) {
|
|
794
|
+
throw new Error(`Provider profile "${profileName}" was not found`);
|
|
795
|
+
}
|
|
796
|
+
return {
|
|
797
|
+
...settings,
|
|
798
|
+
currentProvider: profileName
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function validateProviderProfile(profileName, profile, options = {}) {
|
|
802
|
+
if (!profile.type) {
|
|
803
|
+
throw new Error(`Provider profile "${profileName}" is missing type`);
|
|
804
|
+
}
|
|
805
|
+
if (!profile.model) {
|
|
806
|
+
throw new Error(`Provider profile "${profileName}" is missing model`);
|
|
807
|
+
}
|
|
808
|
+
const definition = findProviderDefinition(options.providerDefinitions ?? [], profile.type);
|
|
809
|
+
if (definition?.requiresApiKey === true && !profile.apiKey && definition.defaults?.apiKey === void 0) {
|
|
810
|
+
throw new Error(`Provider profile "${profileName}" is missing apiKey`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
function buildProviderSetupPatch(input, options = {}) {
|
|
814
|
+
const profile = buildProviderProfile(input, options);
|
|
815
|
+
validateProviderProfile(input.profile, profile, options);
|
|
816
|
+
return {
|
|
817
|
+
...input.setCurrent && { currentProvider: input.profile },
|
|
818
|
+
providers: {
|
|
819
|
+
[input.profile]: profile
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
function buildProviderProfile(input, options = {}) {
|
|
824
|
+
const defaults = getProviderDefaults(input.type, options.providerDefinitions ?? []);
|
|
825
|
+
const apiKey = input.apiKeyEnv !== void 0 ? `$ENV:${input.apiKeyEnv}` : input.apiKey ?? defaults.apiKey;
|
|
826
|
+
const baseURL = input.baseURL ?? defaults.baseURL;
|
|
827
|
+
return {
|
|
828
|
+
type: input.type,
|
|
829
|
+
model: input.model ?? defaults.model,
|
|
830
|
+
...apiKey !== void 0 && { apiKey },
|
|
831
|
+
...baseURL !== void 0 && { baseURL },
|
|
832
|
+
...input.timeout !== void 0 && { timeout: input.timeout }
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
function getProviderDefaults(type, providerDefinitions) {
|
|
836
|
+
return findProviderDefinition(providerDefinitions, type)?.defaults ?? {};
|
|
837
|
+
}
|
|
838
|
+
function mergeProviderPatch(settings, patch) {
|
|
839
|
+
const [profileName, profile] = Object.entries(patch.providers)[0] ?? [];
|
|
840
|
+
if (!profileName || !profile) {
|
|
841
|
+
return settings;
|
|
842
|
+
}
|
|
843
|
+
const withProfile = upsertProviderProfile(settings, profileName, profile);
|
|
844
|
+
return patch.currentProvider ? setCurrentProvider(withProfile, patch.currentProvider) : withProfile;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// src/utils/provider-configuration.ts
|
|
848
|
+
function readProviderDocument(settingsPath) {
|
|
849
|
+
return readSettings(settingsPath);
|
|
850
|
+
}
|
|
851
|
+
function applyProviderConfiguration(settingsPath, input, options = {}) {
|
|
852
|
+
const settings = readProviderDocument(settingsPath);
|
|
853
|
+
const patch = buildProviderSetupPatch(input, options);
|
|
854
|
+
const next = mergeProviderPatch(settings, patch);
|
|
855
|
+
writeSettings(settingsPath, next);
|
|
856
|
+
return next;
|
|
857
|
+
}
|
|
858
|
+
function applyProviderSwitch(settingsPath, profileName, options = {}) {
|
|
859
|
+
const settings = readProviderDocument(settingsPath);
|
|
860
|
+
const hasLocalProfile = settings.providers?.[profileName] !== void 0;
|
|
861
|
+
const hasKnownProfile = options.knownProviders?.[profileName] !== void 0;
|
|
862
|
+
const next = hasLocalProfile || hasKnownProfile ? { ...settings, currentProvider: profileName } : setCurrentProvider(settings, profileName);
|
|
863
|
+
writeSettings(settingsPath, next);
|
|
864
|
+
return next;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// src/utils/provider-setup-flow.ts
|
|
868
|
+
function createProviderSetupFlow(type, providerDefinitions) {
|
|
869
|
+
return {
|
|
870
|
+
type,
|
|
871
|
+
steps: getProviderSetupSteps(type, providerDefinitions),
|
|
872
|
+
stepIndex: 0,
|
|
873
|
+
values: {}
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
function getProviderSetupStep(state) {
|
|
877
|
+
const step = state.steps[state.stepIndex];
|
|
878
|
+
if (step === void 0) {
|
|
879
|
+
throw new Error(`Provider setup step ${state.stepIndex} is out of range`);
|
|
880
|
+
}
|
|
881
|
+
return step;
|
|
882
|
+
}
|
|
883
|
+
function submitProviderSetupValue(state, rawValue) {
|
|
884
|
+
const step = getProviderSetupStep(state);
|
|
885
|
+
const value = rawValue.trim() || step.defaultValue || "";
|
|
886
|
+
const validationMessage = validateProviderSetupValue(step, value);
|
|
887
|
+
if (validationMessage !== void 0) {
|
|
888
|
+
return { status: "error", state, message: validationMessage };
|
|
889
|
+
}
|
|
890
|
+
const nextState = {
|
|
891
|
+
...state,
|
|
892
|
+
stepIndex: state.stepIndex + 1,
|
|
893
|
+
values: { ...state.values, [step.key]: value }
|
|
894
|
+
};
|
|
895
|
+
if (nextState.stepIndex < state.steps.length) {
|
|
896
|
+
return { status: "next", state: nextState };
|
|
897
|
+
}
|
|
898
|
+
return { status: "complete", input: buildProviderSetupInput(nextState) };
|
|
899
|
+
}
|
|
900
|
+
async function runProviderSetupPromptFlow(type, promptInput2, providerDefinitions) {
|
|
901
|
+
let state = createProviderSetupFlow(type, providerDefinitions);
|
|
902
|
+
const stepCount = state.steps.length;
|
|
903
|
+
while (state.stepIndex < stepCount) {
|
|
904
|
+
const step = getProviderSetupStep(state);
|
|
905
|
+
const value = await promptInput2(formatProviderSetupPromptLabel(step), step.masked === true);
|
|
906
|
+
const result = submitProviderSetupValue(state, value);
|
|
907
|
+
if (result.status === "complete") {
|
|
908
|
+
return result.input;
|
|
909
|
+
}
|
|
910
|
+
if (result.status === "error") {
|
|
911
|
+
throw new Error(result.message);
|
|
912
|
+
}
|
|
913
|
+
state = result.state;
|
|
914
|
+
}
|
|
915
|
+
throw new Error("Provider setup flow ended without completion");
|
|
916
|
+
}
|
|
917
|
+
function formatProviderSetupPromptLabel(step) {
|
|
918
|
+
const suffix = step.defaultValue !== void 0 ? ` (default: ${step.defaultValue})` : "";
|
|
919
|
+
return ` ${step.title}${suffix}: `;
|
|
920
|
+
}
|
|
921
|
+
function validateProviderSetupValue(step, value) {
|
|
922
|
+
if (step.required === true && value.length === 0) {
|
|
923
|
+
return "Required";
|
|
924
|
+
}
|
|
925
|
+
return void 0;
|
|
926
|
+
}
|
|
927
|
+
function getProviderSetupSteps(type, providerDefinitions) {
|
|
928
|
+
const definition = findProviderDefinition(providerDefinitions, type);
|
|
929
|
+
if (definition === void 0) {
|
|
930
|
+
throw new Error(
|
|
931
|
+
`Unknown provider: ${type}. Currently supported: ${formatSupportedProviderTypes(providerDefinitions)}`
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
if (definition.setupSteps !== void 0) {
|
|
935
|
+
return [...definition.setupSteps];
|
|
936
|
+
}
|
|
937
|
+
const steps = [
|
|
938
|
+
{
|
|
939
|
+
key: "model",
|
|
940
|
+
title: `${definition.type} model`,
|
|
941
|
+
defaultValue: definition.defaults?.model,
|
|
942
|
+
required: definition.defaults?.model === void 0
|
|
943
|
+
}
|
|
944
|
+
];
|
|
945
|
+
if (definition.defaults?.baseURL !== void 0) {
|
|
946
|
+
steps.unshift({
|
|
947
|
+
key: "baseURL",
|
|
948
|
+
title: `${definition.type} base URL`,
|
|
949
|
+
defaultValue: definition.defaults.baseURL
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
if (definition.requiresApiKey === true) {
|
|
953
|
+
steps.push({
|
|
954
|
+
key: "apiKey",
|
|
955
|
+
title: `${definition.type} API key`,
|
|
956
|
+
defaultValue: definition.defaults?.apiKey,
|
|
957
|
+
required: definition.defaults?.apiKey === void 0,
|
|
958
|
+
masked: true
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
return steps;
|
|
962
|
+
}
|
|
963
|
+
function buildProviderSetupInput(state) {
|
|
964
|
+
return {
|
|
965
|
+
profile: state.type,
|
|
966
|
+
type: state.type,
|
|
967
|
+
model: state.values.model,
|
|
968
|
+
apiKey: state.values.apiKey,
|
|
969
|
+
...state.values.baseURL !== void 0 && { baseURL: state.values.baseURL },
|
|
970
|
+
setCurrent: true
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// src/utils/provider-setup.ts
|
|
975
|
+
function getSettingsPathForScope(cwd, scope) {
|
|
976
|
+
if (scope === void 0 || scope === "user") {
|
|
977
|
+
return getUserSettingsPath();
|
|
978
|
+
}
|
|
979
|
+
if (scope === "project-local") {
|
|
980
|
+
return join4(cwd, ".robota", "settings.local.json");
|
|
981
|
+
}
|
|
982
|
+
throw new Error(`Invalid --settings-scope "${scope}". Valid: user | project-local`);
|
|
983
|
+
}
|
|
984
|
+
function handleProviderConfigurationArgs(cwd, args, providerDefinitions = DEFAULT_PROVIDER_DEFINITIONS) {
|
|
985
|
+
const settingsPath = getSettingsPathForScope(cwd, args.settingsScope);
|
|
986
|
+
if (args.configureProvider) {
|
|
987
|
+
applyProviderConfiguration(settingsPath, buildSetupInputFromArgs(args), {
|
|
988
|
+
providerDefinitions
|
|
989
|
+
});
|
|
990
|
+
process.stdout.write(`Provider profile saved to ${settingsPath}
|
|
991
|
+
`);
|
|
992
|
+
return !args.printMode && args.positional.length === 0;
|
|
993
|
+
}
|
|
994
|
+
if (args.provider && args.setCurrent) {
|
|
995
|
+
applyProviderSwitch(settingsPath, args.provider, {
|
|
996
|
+
knownProviders: readMergedProviderSettings(cwd).providers
|
|
997
|
+
});
|
|
998
|
+
process.stdout.write(`Current provider set to ${args.provider}
|
|
999
|
+
`);
|
|
1000
|
+
return !args.printMode && args.positional.length === 0;
|
|
1001
|
+
}
|
|
1002
|
+
return false;
|
|
1003
|
+
}
|
|
1004
|
+
async function ensureConfig(cwd, args, promptInput2, providerDefinitions = DEFAULT_PROVIDER_DEFINITIONS) {
|
|
1005
|
+
const checks = getSettingsCheckPaths(cwd).map((path) => ({
|
|
1006
|
+
path,
|
|
1007
|
+
status: checkSettingsFile(path, providerDefinitions)
|
|
1008
|
+
}));
|
|
1009
|
+
if (checks.some((check) => check.status === "valid")) {
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
if (!isInteractiveTerminal()) {
|
|
1013
|
+
throw new Error(formatMissingProviderConfigMessage());
|
|
1014
|
+
}
|
|
1015
|
+
await runInteractiveProviderSetup(cwd, args, promptInput2, providerDefinitions);
|
|
1016
|
+
}
|
|
1017
|
+
async function runInteractiveProviderSetup(cwd, args, promptInput2, providerDefinitions = DEFAULT_PROVIDER_DEFINITIONS) {
|
|
1018
|
+
const defaultProviderType = providerDefinitions[0]?.type ?? "";
|
|
1019
|
+
const supportedTypes = providerDefinitions.map((definition) => definition.type).join("/");
|
|
1020
|
+
const providerChoice = await promptInput2(` Provider (${supportedTypes}, default: ${defaultProviderType}): `) || defaultProviderType;
|
|
1021
|
+
const type = parseProviderSetupType(providerChoice);
|
|
1022
|
+
const settingsPath = getSettingsPathForScope(cwd, args.settingsScope);
|
|
1023
|
+
const input = await runProviderSetupPromptFlow(type, promptInput2, providerDefinitions);
|
|
1024
|
+
applyProviderConfiguration(settingsPath, input, {
|
|
1025
|
+
providerDefinitions
|
|
1026
|
+
});
|
|
1027
|
+
const language = await promptInput2(" Response language (ko/en/ja/zh, default: en): ");
|
|
1028
|
+
if (language) {
|
|
1029
|
+
const settings = readSettings(settingsPath);
|
|
1030
|
+
settings.language = language;
|
|
1031
|
+
writeSettings(settingsPath, settings);
|
|
1032
|
+
}
|
|
1033
|
+
process.stdout.write(`
|
|
1034
|
+
Config saved to ${settingsPath}
|
|
1035
|
+
|
|
1036
|
+
`);
|
|
1037
|
+
}
|
|
1038
|
+
function parseProviderSetupType(value) {
|
|
1039
|
+
return value.trim();
|
|
1040
|
+
}
|
|
1041
|
+
function buildSetupInputFromArgs(args) {
|
|
1042
|
+
const type = args.providerType ?? args.configureProvider;
|
|
1043
|
+
if (!args.configureProvider || !type) {
|
|
1044
|
+
throw new Error("--configure-provider requires a provider profile and --type");
|
|
1045
|
+
}
|
|
1046
|
+
return {
|
|
1047
|
+
profile: args.configureProvider,
|
|
1048
|
+
type,
|
|
1049
|
+
...args.model !== void 0 && { model: args.model },
|
|
1050
|
+
...args.apiKey !== void 0 && { apiKey: args.apiKey },
|
|
1051
|
+
...args.apiKeyEnv !== void 0 && { apiKeyEnv: args.apiKeyEnv },
|
|
1052
|
+
...args.baseURL !== void 0 && { baseURL: args.baseURL },
|
|
1053
|
+
setCurrent: args.setCurrent
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
function getSettingsCheckPaths(cwd) {
|
|
1057
|
+
return [
|
|
1058
|
+
getUserSettingsPath(),
|
|
1059
|
+
join4(homedir(), ".claude", "settings.json"),
|
|
1060
|
+
join4(cwd, ".robota", "settings.json"),
|
|
1061
|
+
join4(cwd, ".robota", "settings.local.json"),
|
|
1062
|
+
join4(cwd, ".claude", "settings.json"),
|
|
1063
|
+
join4(cwd, ".claude", "settings.local.json")
|
|
1064
|
+
];
|
|
1065
|
+
}
|
|
1066
|
+
function isInteractiveTerminal() {
|
|
1067
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
1068
|
+
}
|
|
1069
|
+
function formatMissingProviderConfigMessage() {
|
|
1070
|
+
return [
|
|
1071
|
+
"No provider configuration found.",
|
|
1072
|
+
"Run `robota --configure` in an interactive terminal, or configure a provider:",
|
|
1073
|
+
" robota --configure-provider gemma --type gemma --base-url http://localhost:1234/v1 --model supergemma4-26b-uncensored-v2 --api-key lm-studio --set-current",
|
|
1074
|
+
" robota --configure-provider openai --type openai --model <openai-compatible-model> --api-key-env OPENAI_API_KEY --set-current"
|
|
1075
|
+
].join("\n");
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// src/cli.ts
|
|
1079
|
+
import { createHeadlessTransport } from "@robota-sdk/agent-transport-headless";
|
|
1080
|
+
|
|
1081
|
+
// src/ui/render.tsx
|
|
1082
|
+
import { render } from "ink";
|
|
1083
|
+
|
|
1084
|
+
// src/ui/App.tsx
|
|
1085
|
+
import { useState as useState14, useEffect as useEffect4 } from "react";
|
|
1086
|
+
import { Box as Box14, Text as Text16, useApp as useApp2, useInput as useInput8 } from "ink";
|
|
1087
|
+
import { getModelName as getModelName2, createSystemMessage as createSystemMessage4, messageToHistoryEntry as messageToHistoryEntry4 } from "@robota-sdk/agent-core";
|
|
1088
|
+
|
|
1089
|
+
// src/ui/hooks/useInteractiveSession.ts
|
|
1090
|
+
import { useState, useRef, useCallback as useCallback2, useEffect } from "react";
|
|
1091
|
+
import { homedir as homedir2 } from "os";
|
|
1092
|
+
import { join as join5 } from "path";
|
|
1093
|
+
import {
|
|
1094
|
+
InteractiveSession,
|
|
1095
|
+
CommandRegistry,
|
|
1096
|
+
BuiltinCommandSource,
|
|
1097
|
+
SkillCommandSource,
|
|
1098
|
+
PluginCommandSource,
|
|
1099
|
+
BundlePluginLoader
|
|
1100
|
+
} from "@robota-sdk/agent-sdk";
|
|
1101
|
+
import { createSystemMessage as createSystemMessage2, messageToHistoryEntry as messageToHistoryEntry2 } from "@robota-sdk/agent-core";
|
|
1102
|
+
|
|
1103
|
+
// src/ui/background-task-view-model.ts
|
|
1104
|
+
var BACKGROUND_PREVIEW_LENGTH = 120;
|
|
1105
|
+
var BACKGROUND_PREVIEW_WHITESPACE = /\s+/g;
|
|
1106
|
+
var BACKGROUND_PREVIEW_SEPARATOR = " ";
|
|
1107
|
+
var SUCCESS_EXIT_CODE = 0;
|
|
1108
|
+
function toBackgroundTaskViewModel(state, partialText) {
|
|
1109
|
+
return {
|
|
1110
|
+
id: state.id,
|
|
1111
|
+
kind: state.kind,
|
|
1112
|
+
label: state.label,
|
|
1113
|
+
status: state.status,
|
|
1114
|
+
statusLabel: getBackgroundTaskStatusLabel(state),
|
|
1115
|
+
mode: state.mode,
|
|
1116
|
+
currentAction: state.currentAction,
|
|
1117
|
+
unread: state.unread,
|
|
1118
|
+
preview: trimBackgroundPreview(state.promptPreview ?? state.commandPreview) ?? "",
|
|
1119
|
+
resultPreview: trimBackgroundPreview(state.result?.output ?? partialText),
|
|
1120
|
+
errorPreview: trimBackgroundPreview(state.error?.message),
|
|
1121
|
+
startedAt: state.startedAt,
|
|
1122
|
+
lastActivityAt: state.lastActivityAt,
|
|
1123
|
+
timeoutReason: state.timeoutReason
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
function getBackgroundTaskStatusLabel(state) {
|
|
1127
|
+
if (state.status === "failed" && state.timeoutReason) {
|
|
1128
|
+
if (state.timeoutReason === "idle" || state.timeoutReason === "max_runtime") {
|
|
1129
|
+
return "timed out";
|
|
1130
|
+
}
|
|
1131
|
+
return state.timeoutReason.replace(/_/g, " ");
|
|
1132
|
+
}
|
|
1133
|
+
return state.status;
|
|
1134
|
+
}
|
|
1135
|
+
function shouldHideAtNextUserTurn(state) {
|
|
1136
|
+
return state.status === "completed" && !state.error && (state.result?.exitCode === void 0 || state.result.exitCode === SUCCESS_EXIT_CODE) && !state.result?.signalCode && !state.worktreePath && !state.branchName;
|
|
1137
|
+
}
|
|
1138
|
+
function trimBackgroundPreview(value) {
|
|
1139
|
+
if (!value) return void 0;
|
|
1140
|
+
const preview = value.trim().replace(BACKGROUND_PREVIEW_WHITESPACE, BACKGROUND_PREVIEW_SEPARATOR);
|
|
1141
|
+
if (!preview) return void 0;
|
|
1142
|
+
return preview.length > BACKGROUND_PREVIEW_LENGTH ? `${preview.slice(0, BACKGROUND_PREVIEW_LENGTH)}...` : preview;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// src/ui/tui-state-manager.ts
|
|
1146
|
+
var MAX_RENDERED_MESSAGES = 100;
|
|
1147
|
+
var STREAMING_DEBOUNCE_MS = 300;
|
|
1148
|
+
function createDebouncedNotify(notify, ms) {
|
|
1149
|
+
let timer = null;
|
|
1150
|
+
return {
|
|
1151
|
+
schedule() {
|
|
1152
|
+
if (!timer) {
|
|
1153
|
+
timer = setTimeout(() => {
|
|
1154
|
+
timer = null;
|
|
1155
|
+
notify();
|
|
1156
|
+
}, ms);
|
|
1157
|
+
}
|
|
1158
|
+
},
|
|
1159
|
+
flush() {
|
|
1160
|
+
if (timer) {
|
|
1161
|
+
clearTimeout(timer);
|
|
1162
|
+
timer = null;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
var TuiStateManager = class {
|
|
1168
|
+
// ── Rendering state ───────────────────────────────────────────
|
|
1169
|
+
history = [];
|
|
1170
|
+
streamingText = "";
|
|
1171
|
+
activeTools = [];
|
|
1172
|
+
isThinking = false;
|
|
1173
|
+
isAborting = false;
|
|
1174
|
+
pendingPrompt = null;
|
|
1175
|
+
contextState = { percentage: 0, usedTokens: 0, maxTokens: 0 };
|
|
1176
|
+
backgroundTasks = [];
|
|
1177
|
+
/** Called after any state change. React hook sets this to trigger re-render. */
|
|
1178
|
+
onChange = null;
|
|
1179
|
+
// ── Internal ──────────────────────────────────────────────────
|
|
1180
|
+
streamBuf = "";
|
|
1181
|
+
backgroundTextBuffers = /* @__PURE__ */ new Map();
|
|
1182
|
+
backgroundTasksHiddenOnNextTurn = /* @__PURE__ */ new Set();
|
|
1183
|
+
debouncedStreamNotify = createDebouncedNotify(() => this.notify(), STREAMING_DEBOUNCE_MS);
|
|
1184
|
+
notify() {
|
|
1185
|
+
this.onChange?.();
|
|
1186
|
+
}
|
|
1187
|
+
// ── Event handlers (InteractiveSession → state) ───────────────
|
|
1188
|
+
onTextDelta = (delta) => {
|
|
1189
|
+
this.streamBuf += delta;
|
|
1190
|
+
this.streamingText = this.streamBuf;
|
|
1191
|
+
this.debouncedStreamNotify.schedule();
|
|
1192
|
+
};
|
|
1193
|
+
onToolStart = (state) => {
|
|
1194
|
+
this.activeTools = [...this.activeTools, state];
|
|
1195
|
+
this.notify();
|
|
1196
|
+
};
|
|
1197
|
+
onToolEnd = (state) => {
|
|
1198
|
+
const idx = this.activeTools.findIndex((t) => t.toolName === state.toolName && t.isRunning);
|
|
1199
|
+
if (idx !== -1) {
|
|
1200
|
+
const updated = [...this.activeTools];
|
|
1201
|
+
updated[idx] = state;
|
|
1202
|
+
this.activeTools = updated;
|
|
1203
|
+
}
|
|
1204
|
+
this.notify();
|
|
1205
|
+
};
|
|
1206
|
+
onThinking = (thinking) => {
|
|
1207
|
+
this.isThinking = thinking;
|
|
1208
|
+
if (thinking) {
|
|
1209
|
+
this.debouncedStreamNotify.flush();
|
|
1210
|
+
this.streamBuf = "";
|
|
1211
|
+
this.streamingText = "";
|
|
1212
|
+
this.activeTools = [];
|
|
1213
|
+
} else {
|
|
1214
|
+
this.isAborting = false;
|
|
1215
|
+
}
|
|
1216
|
+
this.notify();
|
|
1217
|
+
};
|
|
1218
|
+
onComplete = (result) => {
|
|
1219
|
+
this.debouncedStreamNotify.flush();
|
|
1220
|
+
this.streamBuf = "";
|
|
1221
|
+
this.streamingText = "";
|
|
1222
|
+
this.activeTools = [];
|
|
1223
|
+
this.contextState = {
|
|
1224
|
+
percentage: result.contextState.usedPercentage,
|
|
1225
|
+
usedTokens: result.contextState.usedTokens,
|
|
1226
|
+
maxTokens: result.contextState.maxTokens
|
|
1227
|
+
};
|
|
1228
|
+
this.notify();
|
|
1229
|
+
};
|
|
1230
|
+
onInterrupted = () => {
|
|
1231
|
+
this.debouncedStreamNotify.flush();
|
|
1232
|
+
this.streamBuf = "";
|
|
1233
|
+
this.streamingText = "";
|
|
1234
|
+
this.activeTools = [];
|
|
1235
|
+
this.notify();
|
|
1236
|
+
};
|
|
1237
|
+
onError = () => {
|
|
1238
|
+
this.debouncedStreamNotify.flush();
|
|
1239
|
+
this.streamBuf = "";
|
|
1240
|
+
this.streamingText = "";
|
|
1241
|
+
this.activeTools = [];
|
|
1242
|
+
this.notify();
|
|
1243
|
+
};
|
|
1244
|
+
onBackgroundTaskEvent = (event) => {
|
|
1245
|
+
if ("task" in event) {
|
|
1246
|
+
this.upsertBackgroundTask(event.task);
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
if (event.type === "background_task_closed") {
|
|
1250
|
+
this.backgroundTextBuffers.delete(event.taskId);
|
|
1251
|
+
this.backgroundTasksHiddenOnNextTurn.delete(event.taskId);
|
|
1252
|
+
this.backgroundTasks = this.backgroundTasks.filter((task) => task.id !== event.taskId);
|
|
1253
|
+
this.notify();
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
if (event.type === "background_task_text_delta") {
|
|
1257
|
+
this.appendBackgroundTaskText(event.taskId, event.delta);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
if (event.type === "background_task_tool_start") {
|
|
1261
|
+
this.updateBackgroundTaskAction(event.taskId, event.firstArg ?? event.toolName);
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
if (event.type === "background_task_tool_end") {
|
|
1265
|
+
this.updateBackgroundTaskAction(event.taskId, event.success ? void 0 : event.error);
|
|
1266
|
+
}
|
|
1267
|
+
};
|
|
1268
|
+
// ── State updates from external sources ───────────────────────
|
|
1269
|
+
/** Sync history from InteractiveSession */
|
|
1270
|
+
syncHistory(entries) {
|
|
1271
|
+
if (entries.length === 0) return;
|
|
1272
|
+
this.history = entries.length > MAX_RENDERED_MESSAGES ? entries.slice(-MAX_RENDERED_MESSAGES) : [...entries];
|
|
1273
|
+
this.notify();
|
|
1274
|
+
}
|
|
1275
|
+
/** Add a single history entry */
|
|
1276
|
+
addEntry(entry) {
|
|
1277
|
+
const updated = [...this.history, entry];
|
|
1278
|
+
this.history = updated.length > MAX_RENDERED_MESSAGES ? updated.slice(-MAX_RENDERED_MESSAGES) : updated;
|
|
1279
|
+
this.notify();
|
|
1280
|
+
}
|
|
1281
|
+
/** Update pending prompt state */
|
|
1282
|
+
setPendingPrompt(prompt) {
|
|
1283
|
+
this.pendingPrompt = prompt;
|
|
1284
|
+
this.notify();
|
|
1285
|
+
}
|
|
1286
|
+
/** Set aborting flag */
|
|
1287
|
+
setAborting(aborting) {
|
|
1288
|
+
this.isAborting = aborting;
|
|
1289
|
+
this.notify();
|
|
1290
|
+
}
|
|
1291
|
+
/** Update context state */
|
|
1292
|
+
setContextState(state) {
|
|
1293
|
+
this.contextState = state;
|
|
1294
|
+
this.notify();
|
|
1295
|
+
}
|
|
1296
|
+
onUserTurnAccepted() {
|
|
1297
|
+
if (this.backgroundTasksHiddenOnNextTurn.size === 0) return;
|
|
1298
|
+
const visible = this.backgroundTasks.filter(
|
|
1299
|
+
(task) => !this.backgroundTasksHiddenOnNextTurn.has(task.id)
|
|
1300
|
+
);
|
|
1301
|
+
this.backgroundTasksHiddenOnNextTurn.clear();
|
|
1302
|
+
if (visible.length === this.backgroundTasks.length) return;
|
|
1303
|
+
this.backgroundTasks = visible;
|
|
1304
|
+
this.notify();
|
|
1305
|
+
}
|
|
1306
|
+
upsertBackgroundTask(state) {
|
|
1307
|
+
const partialText = state.result ? void 0 : this.backgroundTextBuffers.get(state.id);
|
|
1308
|
+
const viewModel = toBackgroundTaskViewModel(state, partialText);
|
|
1309
|
+
const index = this.backgroundTasks.findIndex((task) => task.id === state.id);
|
|
1310
|
+
if (index === -1) {
|
|
1311
|
+
this.backgroundTasks = [...this.backgroundTasks, viewModel];
|
|
1312
|
+
} else {
|
|
1313
|
+
const updated = [...this.backgroundTasks];
|
|
1314
|
+
updated[index] = viewModel;
|
|
1315
|
+
this.backgroundTasks = updated;
|
|
1316
|
+
}
|
|
1317
|
+
if (state.status === "completed" || state.status === "failed" || state.status === "cancelled") {
|
|
1318
|
+
this.backgroundTextBuffers.delete(state.id);
|
|
1319
|
+
}
|
|
1320
|
+
if (shouldHideAtNextUserTurn(state)) {
|
|
1321
|
+
this.backgroundTasksHiddenOnNextTurn.add(state.id);
|
|
1322
|
+
} else {
|
|
1323
|
+
this.backgroundTasksHiddenOnNextTurn.delete(state.id);
|
|
1324
|
+
}
|
|
1325
|
+
this.notify();
|
|
1326
|
+
}
|
|
1327
|
+
appendBackgroundTaskText(taskId, delta) {
|
|
1328
|
+
const nextText = `${this.backgroundTextBuffers.get(taskId) ?? ""}${delta}`;
|
|
1329
|
+
this.backgroundTextBuffers.set(taskId, nextText);
|
|
1330
|
+
this.backgroundTasks = this.backgroundTasks.map(
|
|
1331
|
+
(task) => task.id === taskId ? { ...task, resultPreview: trimBackgroundPreview(nextText) } : task
|
|
1332
|
+
);
|
|
1333
|
+
this.notify();
|
|
1334
|
+
}
|
|
1335
|
+
updateBackgroundTaskAction(taskId, currentAction) {
|
|
1336
|
+
this.backgroundTasks = this.backgroundTasks.map(
|
|
1337
|
+
(task) => task.id === taskId ? { ...task, currentAction } : task
|
|
1338
|
+
);
|
|
1339
|
+
this.notify();
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1343
|
+
// src/ui/hooks/useSlashRouting.ts
|
|
1344
|
+
import { useCallback } from "react";
|
|
1345
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1346
|
+
import { createSystemMessage, messageToHistoryEntry } from "@robota-sdk/agent-core";
|
|
1347
|
+
|
|
1348
|
+
// src/utils/provider-command.ts
|
|
1349
|
+
async function handleProviderCommand(cwd, args, deps = {}) {
|
|
1350
|
+
const settings = readMergedProviderSettings(cwd);
|
|
1351
|
+
const providerDefinitions = deps.providerDefinitions ?? DEFAULT_PROVIDER_DEFINITIONS;
|
|
1352
|
+
const [subcommand = "current", profileArg] = args.trim().split(/\s+/);
|
|
1353
|
+
if (subcommand === "list") {
|
|
1354
|
+
return {
|
|
1355
|
+
message: formatProviderList(settings.currentProvider, settings.providers),
|
|
1356
|
+
success: true
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
if (subcommand === "current" || subcommand === "") {
|
|
1360
|
+
return {
|
|
1361
|
+
message: formatCurrentProvider(settings.currentProvider, settings.providers),
|
|
1362
|
+
success: true
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
if (subcommand === "use") {
|
|
1366
|
+
return buildProviderSwitch(settings.providers, profileArg);
|
|
1367
|
+
}
|
|
1368
|
+
if (subcommand === "test") {
|
|
1369
|
+
return await testProvider(settings.currentProvider, settings.providers, profileArg, deps);
|
|
1370
|
+
}
|
|
1371
|
+
if (subcommand === "add") {
|
|
1372
|
+
return buildProviderSetup(profileArg, providerDefinitions);
|
|
1373
|
+
}
|
|
1374
|
+
return {
|
|
1375
|
+
message: "Usage: provider [current|list|use <profile>|add <type>|test [profile]]",
|
|
1376
|
+
success: false
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
function formatProviderList(currentProvider, providers) {
|
|
1380
|
+
const entries = Object.entries(providers ?? {});
|
|
1381
|
+
if (entries.length === 0) {
|
|
1382
|
+
return "No provider profiles configured.";
|
|
1383
|
+
}
|
|
1384
|
+
return entries.map(([name, profile]) => {
|
|
1385
|
+
const marker = name === currentProvider ? "*" : "-";
|
|
1386
|
+
return `${marker} ${name}: ${profile.type ?? "unknown"} ${profile.model ?? "(no model)"}`;
|
|
1387
|
+
}).join("\n");
|
|
1388
|
+
}
|
|
1389
|
+
function formatCurrentProvider(currentProvider, providers) {
|
|
1390
|
+
if (!currentProvider) {
|
|
1391
|
+
return "No current provider configured.";
|
|
1392
|
+
}
|
|
1393
|
+
const profile = providers?.[currentProvider];
|
|
1394
|
+
if (!profile) {
|
|
1395
|
+
return `Current provider "${currentProvider}" was not found in providers.`;
|
|
1396
|
+
}
|
|
1397
|
+
return [
|
|
1398
|
+
`Current provider: ${currentProvider}`,
|
|
1399
|
+
`Type: ${profile.type ?? "unknown"}`,
|
|
1400
|
+
`Model: ${profile.model ?? "(no model)"}`,
|
|
1401
|
+
...profile.baseURL ? [`Base URL: ${profile.baseURL}`] : []
|
|
1402
|
+
].join("\n");
|
|
1403
|
+
}
|
|
1404
|
+
function buildProviderSwitch(providers, profileName) {
|
|
1405
|
+
if (!profileName) {
|
|
1406
|
+
return { message: "Usage: provider use <profile>", success: false };
|
|
1407
|
+
}
|
|
1408
|
+
if (!providers?.[profileName]) {
|
|
1409
|
+
return { message: `Provider profile "${profileName}" was not found.`, success: false };
|
|
1410
|
+
}
|
|
1411
|
+
return {
|
|
1412
|
+
message: `Provider change requested: ${profileName}`,
|
|
1413
|
+
success: true,
|
|
1414
|
+
data: { providerSwitch: { profile: profileName } }
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
function buildProviderSetup(type, providerDefinitions) {
|
|
1418
|
+
if (!type || findProviderDefinition(providerDefinitions, type) === void 0) {
|
|
1419
|
+
return {
|
|
1420
|
+
message: `Usage: provider add <type>. Supported: ${formatSupportedProviderTypes(providerDefinitions)}`,
|
|
1421
|
+
success: false
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
return {
|
|
1425
|
+
message: `Provider setup requested: ${type}`,
|
|
1426
|
+
success: true,
|
|
1427
|
+
data: { providerSetup: { type } }
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
async function testProvider(currentProvider, providers, profileArg, deps) {
|
|
1431
|
+
const profileName = profileArg ?? currentProvider;
|
|
1432
|
+
if (!profileName) {
|
|
1433
|
+
return { message: "No provider profile selected.", success: false };
|
|
1434
|
+
}
|
|
1435
|
+
const profile = providers?.[profileName];
|
|
1436
|
+
if (!profile) {
|
|
1437
|
+
return { message: `Provider profile "${profileName}" was not found.`, success: false };
|
|
1438
|
+
}
|
|
1439
|
+
try {
|
|
1440
|
+
validateProviderProfile(profileName, profile, {
|
|
1441
|
+
providerDefinitions: deps.providerDefinitions ?? DEFAULT_PROVIDER_DEFINITIONS
|
|
1442
|
+
});
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
return { message: error instanceof Error ? error.message : String(error), success: false };
|
|
1445
|
+
}
|
|
1446
|
+
const providerDefinitions = deps.providerDefinitions ?? DEFAULT_PROVIDER_DEFINITIONS;
|
|
1447
|
+
const definition = profile.type ? findProviderDefinition(providerDefinitions, profile.type) : void 0;
|
|
1448
|
+
const probe = deps.probe ?? definition?.probeProfile ?? probeProviderProfile;
|
|
1449
|
+
const result = await probe(profile);
|
|
1450
|
+
return {
|
|
1451
|
+
message: result.ok ? `Provider "${profileName}" test passed: ${result.message}` : `Provider "${profileName}" test failed: ${result.message}; manual configuration can continue.`,
|
|
1452
|
+
success: true,
|
|
1453
|
+
data: { providerTest: { profile: profileName } }
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
async function probeProviderProfile(profile) {
|
|
1457
|
+
void profile;
|
|
1458
|
+
return { ok: true, message: "Profile fields are valid; no endpoint probe configured." };
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// src/ui/hooks/useSlashRouting.ts
|
|
1462
|
+
function useSlashRouting(cwd, interactiveSession, registry, manager, providerDefinitions) {
|
|
1463
|
+
return useCallback(
|
|
1464
|
+
async (input) => {
|
|
1465
|
+
manager.onUserTurnAccepted();
|
|
1466
|
+
if (!input.startsWith("/")) {
|
|
1467
|
+
await interactiveSession.submit(input);
|
|
1468
|
+
manager.setPendingPrompt(interactiveSession.getPendingPrompt());
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
const parts = input.slice(1).split(/\s+/);
|
|
1472
|
+
const cmd = parts[0]?.toLowerCase() ?? "";
|
|
1473
|
+
const args = parts.slice(1).join(" ");
|
|
1474
|
+
if (cmd === "provider") {
|
|
1475
|
+
await routeProviderCommand(cwd, args, interactiveSession, manager, providerDefinitions);
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
const result = await interactiveSession.executeCommand(cmd, args);
|
|
1479
|
+
if (result) {
|
|
1480
|
+
applySystemCommandResult(result, interactiveSession, manager);
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
if (await routeSkillCommand(input, cmd, registry, interactiveSession, manager)) {
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
if (routeTuiCommand(cmd, interactiveSession)) {
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
manager.addEntry(
|
|
1490
|
+
messageToHistoryEntry(
|
|
1491
|
+
createSystemMessage(`Unknown command "/${cmd}". Type /help for help.`)
|
|
1492
|
+
)
|
|
1493
|
+
);
|
|
1494
|
+
},
|
|
1495
|
+
[cwd, interactiveSession, registry, manager, providerDefinitions]
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
async function routeProviderCommand(cwd, args, interactiveSession, manager, providerDefinitions) {
|
|
1499
|
+
const result = await handleProviderCommand(cwd, args, { providerDefinitions });
|
|
1500
|
+
manager.addEntry(messageToHistoryEntry(createSystemMessage(result.message)));
|
|
1501
|
+
const providerSwitch = result.data?.providerSwitch;
|
|
1502
|
+
if (providerSwitch?.profile) {
|
|
1503
|
+
getEffects(interactiveSession)._pendingProviderProfile = providerSwitch.profile;
|
|
1504
|
+
}
|
|
1505
|
+
const providerSetup = result.data?.providerSetup;
|
|
1506
|
+
if (providerSetup?.type) {
|
|
1507
|
+
getEffects(interactiveSession)._pendingProviderSetupType = providerSetup.type;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
function applySystemCommandResult(result, interactiveSession, manager) {
|
|
1511
|
+
manager.addEntry(messageToHistoryEntry(createSystemMessage(result.message)));
|
|
1512
|
+
const data = result.data;
|
|
1513
|
+
const effects = getEffects(interactiveSession);
|
|
1514
|
+
if (typeof data?.modelId === "string") {
|
|
1515
|
+
effects._pendingModelId = data.modelId;
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
if (typeof data?.language === "string") {
|
|
1519
|
+
effects._pendingLanguage = data.language;
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
if (data?.resetRequested === true) {
|
|
1523
|
+
effects._resetRequested = true;
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
if (data?.triggerResumePicker === true) {
|
|
1527
|
+
effects._triggerResumePicker = true;
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
if (typeof data?.name === "string") {
|
|
1531
|
+
effects._sessionName = data.name;
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
const ctx = interactiveSession.getContextState();
|
|
1535
|
+
manager.setContextState({
|
|
1536
|
+
percentage: ctx.usedPercentage,
|
|
1537
|
+
usedTokens: ctx.usedTokens,
|
|
1538
|
+
maxTokens: ctx.maxTokens
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
async function routeSkillCommand(input, cmd, registry, interactiveSession, manager) {
|
|
1542
|
+
const skillCmd = registry.getCommands().find((c) => c.name === cmd && (c.source === "skill" || c.source === "plugin"));
|
|
1543
|
+
if (!skillCmd) {
|
|
1544
|
+
return false;
|
|
1545
|
+
}
|
|
1546
|
+
manager.addEntry({
|
|
1547
|
+
id: randomUUID2(),
|
|
1548
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1549
|
+
category: "event",
|
|
1550
|
+
type: "skill-invocation",
|
|
1551
|
+
data: {
|
|
1552
|
+
skillName: cmd,
|
|
1553
|
+
source: skillCmd.source,
|
|
1554
|
+
message: `Invoking ${skillCmd.source}: ${cmd}`
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1557
|
+
const args = input.slice(1 + cmd.length).trimStart();
|
|
1558
|
+
const qualifiedName = registry.resolveQualifiedName(cmd);
|
|
1559
|
+
const hookInput = qualifiedName ? `/${qualifiedName}${input.slice(1 + cmd.length)}` : input;
|
|
1560
|
+
await interactiveSession.executeSkillCommand(skillCmd, args, input, hookInput);
|
|
1561
|
+
manager.setPendingPrompt(interactiveSession.getPendingPrompt());
|
|
1562
|
+
return true;
|
|
1563
|
+
}
|
|
1564
|
+
function routeTuiCommand(cmd, interactiveSession) {
|
|
1565
|
+
if (cmd === "exit") {
|
|
1566
|
+
getEffects(interactiveSession)._exitRequested = true;
|
|
1567
|
+
return true;
|
|
1568
|
+
}
|
|
1569
|
+
if (cmd === "plugin") {
|
|
1570
|
+
getEffects(interactiveSession)._triggerPluginTUI = true;
|
|
1571
|
+
return true;
|
|
1572
|
+
}
|
|
1573
|
+
return false;
|
|
1574
|
+
}
|
|
1575
|
+
function getEffects(interactiveSession) {
|
|
1576
|
+
return interactiveSession;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// src/ui/hooks/useInteractiveSession.ts
|
|
1580
|
+
function initializeSession(props, permissionHandler) {
|
|
1581
|
+
const interactiveSession = new InteractiveSession({
|
|
1582
|
+
cwd: props.cwd,
|
|
1583
|
+
provider: props.provider,
|
|
1584
|
+
permissionMode: props.permissionMode,
|
|
1585
|
+
maxTurns: props.maxTurns,
|
|
1586
|
+
permissionHandler,
|
|
1587
|
+
sessionStore: props.sessionStore,
|
|
1588
|
+
resumeSessionId: props.resumeSessionId,
|
|
1589
|
+
forkSession: props.forkSession,
|
|
1590
|
+
sessionName: props.sessionName,
|
|
1591
|
+
backgroundTaskRunners: props.backgroundTaskRunners,
|
|
1592
|
+
subagentRunnerFactory: props.subagentRunnerFactory,
|
|
1593
|
+
commandModules: props.commandModules
|
|
1594
|
+
});
|
|
1595
|
+
const registry = new CommandRegistry();
|
|
1596
|
+
registry.addSource(new BuiltinCommandSource());
|
|
1597
|
+
for (const module of props.commandModules ?? []) {
|
|
1598
|
+
registry.addModule(module);
|
|
1599
|
+
}
|
|
1600
|
+
registry.addSource(new SkillCommandSource(props.cwd));
|
|
1601
|
+
const pluginsDir = join5(homedir2(), ".robota", "plugins");
|
|
1602
|
+
const loader = new BundlePluginLoader(pluginsDir);
|
|
1603
|
+
try {
|
|
1604
|
+
const plugins = loader.loadPluginsSync();
|
|
1605
|
+
if (plugins.length > 0) {
|
|
1606
|
+
registry.addSource(new PluginCommandSource(plugins));
|
|
1607
|
+
}
|
|
1608
|
+
} catch {
|
|
1609
|
+
}
|
|
1610
|
+
const manager = new TuiStateManager();
|
|
1611
|
+
return { interactiveSession, registry, manager };
|
|
1612
|
+
}
|
|
1613
|
+
function useInteractiveSession(props) {
|
|
1614
|
+
const [, forceRender] = useState(0);
|
|
1615
|
+
const [permissionRequest, setPermissionRequest] = useState(null);
|
|
1616
|
+
const [isShuttingDown, setIsShuttingDown] = useState(false);
|
|
1617
|
+
const permissionQueueRef = useRef([]);
|
|
1618
|
+
const processingRef = useRef(false);
|
|
1619
|
+
const processNextPermission = useCallback2(() => {
|
|
1620
|
+
if (processingRef.current) return;
|
|
1621
|
+
const next = permissionQueueRef.current[0];
|
|
1622
|
+
if (!next) {
|
|
1623
|
+
setPermissionRequest(null);
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
processingRef.current = true;
|
|
1627
|
+
setPermissionRequest({
|
|
1628
|
+
toolName: next.toolName,
|
|
1629
|
+
toolArgs: next.toolArgs,
|
|
1630
|
+
resolve: (result) => {
|
|
1631
|
+
permissionQueueRef.current.shift();
|
|
1632
|
+
processingRef.current = false;
|
|
1633
|
+
setPermissionRequest(null);
|
|
1634
|
+
next.resolve(result);
|
|
1635
|
+
setTimeout(() => processNextPermission(), 0);
|
|
1636
|
+
}
|
|
1637
|
+
});
|
|
1638
|
+
}, []);
|
|
1639
|
+
const permissionHandler = useCallback2(
|
|
1640
|
+
(toolName, toolArgs) => new Promise((resolve) => {
|
|
1641
|
+
permissionQueueRef.current.push({ toolName, toolArgs, resolve });
|
|
1642
|
+
processNextPermission();
|
|
1643
|
+
}),
|
|
1644
|
+
[processNextPermission]
|
|
1645
|
+
);
|
|
1646
|
+
const stateRef = useRef(null);
|
|
1647
|
+
if (stateRef.current === null) {
|
|
1648
|
+
stateRef.current = initializeSession(props, permissionHandler);
|
|
1649
|
+
}
|
|
1650
|
+
const { interactiveSession, registry, manager } = stateRef.current;
|
|
1651
|
+
manager.onChange = () => forceRender((n) => n + 1);
|
|
1652
|
+
if (manager.history.length === 0) {
|
|
1653
|
+
const restored = interactiveSession.getFullHistory();
|
|
1654
|
+
if (restored.length > 0) {
|
|
1655
|
+
manager.syncHistory(restored);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
useEffect(() => {
|
|
1659
|
+
interactiveSession.on("text_delta", manager.onTextDelta);
|
|
1660
|
+
interactiveSession.on("tool_start", manager.onToolStart);
|
|
1661
|
+
interactiveSession.on("tool_end", manager.onToolEnd);
|
|
1662
|
+
interactiveSession.on("thinking", manager.onThinking);
|
|
1663
|
+
interactiveSession.on("complete", manager.onComplete);
|
|
1664
|
+
interactiveSession.on("interrupted", manager.onInterrupted);
|
|
1665
|
+
interactiveSession.on("error", manager.onError);
|
|
1666
|
+
interactiveSession.on("background_task_event", manager.onBackgroundTaskEvent);
|
|
1667
|
+
const initCheck = setInterval(() => {
|
|
1668
|
+
try {
|
|
1669
|
+
const ctx = interactiveSession.getContextState();
|
|
1670
|
+
manager.setContextState({
|
|
1671
|
+
percentage: ctx.usedPercentage,
|
|
1672
|
+
usedTokens: ctx.usedTokens,
|
|
1673
|
+
maxTokens: ctx.maxTokens
|
|
1674
|
+
});
|
|
1675
|
+
const restored = interactiveSession.getFullHistory();
|
|
1676
|
+
if (restored.length > 0) {
|
|
1677
|
+
manager.syncHistory(restored);
|
|
1678
|
+
}
|
|
1679
|
+
clearInterval(initCheck);
|
|
1680
|
+
} catch {
|
|
1681
|
+
}
|
|
1682
|
+
}, 200);
|
|
1683
|
+
return () => {
|
|
1684
|
+
clearInterval(initCheck);
|
|
1685
|
+
interactiveSession.off("text_delta", manager.onTextDelta);
|
|
1686
|
+
interactiveSession.off("tool_start", manager.onToolStart);
|
|
1687
|
+
interactiveSession.off("tool_end", manager.onToolEnd);
|
|
1688
|
+
interactiveSession.off("thinking", manager.onThinking);
|
|
1689
|
+
interactiveSession.off("complete", manager.onComplete);
|
|
1690
|
+
interactiveSession.off("interrupted", manager.onInterrupted);
|
|
1691
|
+
interactiveSession.off("error", manager.onError);
|
|
1692
|
+
interactiveSession.off("background_task_event", manager.onBackgroundTaskEvent);
|
|
1693
|
+
};
|
|
1694
|
+
}, [interactiveSession, manager]);
|
|
1695
|
+
useEffect(() => {
|
|
1696
|
+
manager.syncHistory(interactiveSession.getFullHistory());
|
|
1697
|
+
if (!manager.isThinking) {
|
|
1698
|
+
manager.setPendingPrompt(interactiveSession.getPendingPrompt());
|
|
1699
|
+
}
|
|
1700
|
+
}, [manager.isThinking, interactiveSession, manager]);
|
|
1701
|
+
const handleSubmit = useSlashRouting(
|
|
1702
|
+
props.cwd,
|
|
1703
|
+
interactiveSession,
|
|
1704
|
+
registry,
|
|
1705
|
+
manager,
|
|
1706
|
+
props.providerDefinitions ?? []
|
|
1707
|
+
);
|
|
1708
|
+
const handleAbort = useCallback2(() => {
|
|
1709
|
+
manager.setAborting(true);
|
|
1710
|
+
interactiveSession.abort();
|
|
1711
|
+
}, [interactiveSession, manager]);
|
|
1712
|
+
const handleCancelQueue = useCallback2(() => {
|
|
1713
|
+
interactiveSession.cancelQueue();
|
|
1714
|
+
manager.setPendingPrompt(null);
|
|
1715
|
+
}, [interactiveSession, manager]);
|
|
1716
|
+
const handleShutdown = useCallback2(
|
|
1717
|
+
async (reason = "prompt_input_exit") => {
|
|
1718
|
+
if (isShuttingDown) return;
|
|
1719
|
+
setIsShuttingDown(true);
|
|
1720
|
+
manager.addEntry(messageToHistoryEntry2(createSystemMessage2("Shutting down...")));
|
|
1721
|
+
await interactiveSession.shutdown({ reason, message: "CLI shutdown" });
|
|
1722
|
+
},
|
|
1723
|
+
[interactiveSession, manager, isShuttingDown]
|
|
1724
|
+
);
|
|
1725
|
+
return {
|
|
1726
|
+
interactiveSession,
|
|
1727
|
+
registry,
|
|
1728
|
+
history: manager.history,
|
|
1729
|
+
addEntry: (entry) => manager.addEntry(entry),
|
|
1730
|
+
streamingText: manager.streamingText,
|
|
1731
|
+
activeTools: manager.activeTools,
|
|
1732
|
+
isThinking: manager.isThinking,
|
|
1733
|
+
isAborting: manager.isAborting,
|
|
1734
|
+
isShuttingDown,
|
|
1735
|
+
pendingPrompt: manager.pendingPrompt,
|
|
1736
|
+
backgroundTasks: manager.backgroundTasks,
|
|
1737
|
+
permissionRequest,
|
|
1738
|
+
contextState: manager.contextState,
|
|
1739
|
+
handleSubmit,
|
|
1740
|
+
handleAbort,
|
|
1741
|
+
handleCancelQueue,
|
|
1742
|
+
handleShutdown
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// src/ui/hooks/usePluginCallbacks.ts
|
|
1747
|
+
import { useMemo } from "react";
|
|
1748
|
+
import { homedir as homedir3 } from "os";
|
|
1749
|
+
import { join as join6 } from "path";
|
|
1750
|
+
import {
|
|
1751
|
+
PluginSettingsStore,
|
|
1752
|
+
BundlePluginLoader as BundlePluginLoader2,
|
|
1753
|
+
BundlePluginInstaller,
|
|
1754
|
+
MarketplaceClient
|
|
1755
|
+
} from "@robota-sdk/agent-sdk";
|
|
1756
|
+
function usePluginCallbacks(cwd) {
|
|
1757
|
+
return useMemo(() => {
|
|
1758
|
+
const home = homedir3();
|
|
1759
|
+
const pluginsDir = join6(home, ".robota", "plugins");
|
|
1760
|
+
const userSettingsPath = join6(home, ".robota", "settings.json");
|
|
1761
|
+
const settingsStore = new PluginSettingsStore(userSettingsPath);
|
|
1762
|
+
const marketplace = new MarketplaceClient({ pluginsDir });
|
|
1763
|
+
const installer = new BundlePluginInstaller({
|
|
1764
|
+
pluginsDir,
|
|
1765
|
+
settingsStore,
|
|
1766
|
+
marketplaceClient: marketplace
|
|
1767
|
+
});
|
|
1768
|
+
const loader = new BundlePluginLoader2(pluginsDir);
|
|
1769
|
+
return {
|
|
1770
|
+
listInstalled: async () => {
|
|
1771
|
+
const plugins = await loader.loadAll();
|
|
1772
|
+
const enabledMap = settingsStore.getEnabledPlugins();
|
|
1773
|
+
return plugins.map((p) => {
|
|
1774
|
+
const parts = p.pluginDir.split("/");
|
|
1775
|
+
const cacheIdx = parts.indexOf("cache");
|
|
1776
|
+
const marketplaceName = cacheIdx >= 0 ? parts[cacheIdx + 1] : "";
|
|
1777
|
+
const fullId = marketplaceName ? `${p.manifest.name}@${marketplaceName}` : p.manifest.name;
|
|
1778
|
+
return {
|
|
1779
|
+
name: fullId,
|
|
1780
|
+
description: p.manifest.description,
|
|
1781
|
+
enabled: enabledMap[fullId] !== false && enabledMap[p.manifest.name] !== false
|
|
1782
|
+
};
|
|
1783
|
+
});
|
|
1784
|
+
},
|
|
1785
|
+
listAvailablePlugins: async (marketplaceName) => {
|
|
1786
|
+
let manifest;
|
|
1787
|
+
try {
|
|
1788
|
+
manifest = marketplace.fetchManifest(marketplaceName);
|
|
1789
|
+
} catch {
|
|
1790
|
+
return [];
|
|
1791
|
+
}
|
|
1792
|
+
const installed = installer.getInstalledPlugins();
|
|
1793
|
+
const installedNames = new Set(Object.values(installed).map((r) => r.pluginName));
|
|
1794
|
+
return manifest.plugins.map((p) => ({
|
|
1795
|
+
name: p.name,
|
|
1796
|
+
description: p.description,
|
|
1797
|
+
installed: installedNames.has(p.name)
|
|
1798
|
+
}));
|
|
1799
|
+
},
|
|
1800
|
+
install: async (pluginId, scope) => {
|
|
1801
|
+
const [name, marketplaceName] = pluginId.split("@");
|
|
1802
|
+
if (!name || !marketplaceName) {
|
|
1803
|
+
throw new Error("Plugin ID must be in format: name@marketplace");
|
|
1804
|
+
}
|
|
1805
|
+
if (scope === "project") {
|
|
1806
|
+
const projectPluginsDir = join6(cwd, ".robota", "plugins");
|
|
1807
|
+
const projectInstaller = new BundlePluginInstaller({
|
|
1808
|
+
pluginsDir: projectPluginsDir,
|
|
1809
|
+
settingsStore,
|
|
1810
|
+
marketplaceClient: marketplace
|
|
1811
|
+
});
|
|
1812
|
+
await projectInstaller.install(name, marketplaceName);
|
|
1813
|
+
} else {
|
|
1814
|
+
await installer.install(name, marketplaceName);
|
|
1815
|
+
}
|
|
1816
|
+
},
|
|
1817
|
+
uninstall: async (pluginId) => {
|
|
1818
|
+
await installer.uninstall(pluginId);
|
|
1819
|
+
},
|
|
1820
|
+
enable: async (pluginId) => {
|
|
1821
|
+
await installer.enable(pluginId);
|
|
1822
|
+
},
|
|
1823
|
+
disable: async (pluginId) => {
|
|
1824
|
+
await installer.disable(pluginId);
|
|
1825
|
+
},
|
|
1826
|
+
marketplaceAdd: async (source) => {
|
|
1827
|
+
if (source.includes("/") && !source.includes(":")) {
|
|
1828
|
+
return marketplace.addMarketplace({ type: "github", repo: source });
|
|
1829
|
+
} else {
|
|
1830
|
+
return marketplace.addMarketplace({ type: "git", url: source });
|
|
1831
|
+
}
|
|
1832
|
+
},
|
|
1833
|
+
marketplaceRemove: async (name) => {
|
|
1834
|
+
const installedFromMarketplace = installer.getPluginsByMarketplace(name);
|
|
1835
|
+
for (const record of installedFromMarketplace) {
|
|
1836
|
+
await installer.uninstall(`${record.pluginName}@${record.marketplace}`);
|
|
1837
|
+
}
|
|
1838
|
+
marketplace.removeMarketplace(name);
|
|
1839
|
+
},
|
|
1840
|
+
marketplaceUpdate: async (name) => {
|
|
1841
|
+
marketplace.updateMarketplace(name);
|
|
1842
|
+
},
|
|
1843
|
+
marketplaceList: async () => {
|
|
1844
|
+
return marketplace.listMarketplaces().map((m) => ({
|
|
1845
|
+
name: m.name,
|
|
1846
|
+
type: m.source.type
|
|
1847
|
+
}));
|
|
1848
|
+
},
|
|
1849
|
+
reloadPlugins: async () => {
|
|
1850
|
+
}
|
|
1851
|
+
};
|
|
1852
|
+
}, [cwd]);
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
// src/ui/hooks/useSideEffects.ts
|
|
1856
|
+
import { useState as useState2, useRef as useRef2, useCallback as useCallback3 } from "react";
|
|
1857
|
+
import { useApp } from "ink";
|
|
1858
|
+
import { createSystemMessage as createSystemMessage3, messageToHistoryEntry as messageToHistoryEntry3, getModelName } from "@robota-sdk/agent-core";
|
|
1859
|
+
var EXIT_DELAY_MS = 500;
|
|
1860
|
+
function useSideEffects({
|
|
1861
|
+
cwd,
|
|
1862
|
+
interactiveSession,
|
|
1863
|
+
addEntry,
|
|
1864
|
+
baseHandleSubmit,
|
|
1865
|
+
setSessionName,
|
|
1866
|
+
providerDefinitions
|
|
1867
|
+
}) {
|
|
1868
|
+
const { exit } = useApp();
|
|
1869
|
+
const [pendingModelId, setPendingModelId] = useState2(null);
|
|
1870
|
+
const pendingModelChangeRef = useRef2(null);
|
|
1871
|
+
const [pendingProviderProfile, setPendingProviderProfile] = useState2(null);
|
|
1872
|
+
const pendingProviderProfileRef = useRef2(null);
|
|
1873
|
+
const [pendingProviderSetupType, setPendingProviderSetupType] = useState2(null);
|
|
1874
|
+
const [showPluginTUI, setShowPluginTUI] = useState2(false);
|
|
1875
|
+
const [showSessionPicker, setShowSessionPicker] = useState2(false);
|
|
1876
|
+
const requestShutdown = useCallback3(
|
|
1877
|
+
(reason, message) => {
|
|
1878
|
+
addEntry(messageToHistoryEntry3(createSystemMessage3("Shutting down...")));
|
|
1879
|
+
setTimeout(() => {
|
|
1880
|
+
void interactiveSession.shutdown({ reason, message }).finally(() => exit());
|
|
1881
|
+
}, EXIT_DELAY_MS);
|
|
1882
|
+
},
|
|
1883
|
+
[interactiveSession, addEntry, exit]
|
|
1884
|
+
);
|
|
1885
|
+
const handleSubmit = useCallback3(
|
|
1886
|
+
async (input) => {
|
|
1887
|
+
await baseHandleSubmit(input);
|
|
1888
|
+
const sideEffects = interactiveSession;
|
|
1889
|
+
if (sideEffects._pendingModelId) {
|
|
1890
|
+
const modelId = sideEffects._pendingModelId;
|
|
1891
|
+
delete sideEffects._pendingModelId;
|
|
1892
|
+
pendingModelChangeRef.current = modelId;
|
|
1893
|
+
setPendingModelId(modelId);
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
if (sideEffects._pendingLanguage) {
|
|
1897
|
+
const lang = sideEffects._pendingLanguage;
|
|
1898
|
+
delete sideEffects._pendingLanguage;
|
|
1899
|
+
const settingsPath = getUserSettingsPath();
|
|
1900
|
+
const settings = readSettings(settingsPath);
|
|
1901
|
+
settings.language = lang;
|
|
1902
|
+
writeSettings(settingsPath, settings);
|
|
1903
|
+
addEntry(
|
|
1904
|
+
messageToHistoryEntry3(createSystemMessage3(`Language set to "${lang}". Restarting...`))
|
|
1905
|
+
);
|
|
1906
|
+
requestShutdown("other", "Language change restart");
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
if (sideEffects._pendingProviderProfile) {
|
|
1910
|
+
const profile = sideEffects._pendingProviderProfile;
|
|
1911
|
+
delete sideEffects._pendingProviderProfile;
|
|
1912
|
+
pendingProviderProfileRef.current = profile;
|
|
1913
|
+
setPendingProviderProfile(profile);
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
if (sideEffects._pendingProviderSetupType) {
|
|
1917
|
+
const type = sideEffects._pendingProviderSetupType;
|
|
1918
|
+
delete sideEffects._pendingProviderSetupType;
|
|
1919
|
+
setPendingProviderSetupType(type);
|
|
1920
|
+
return;
|
|
1921
|
+
}
|
|
1922
|
+
if (sideEffects._resetRequested) {
|
|
1923
|
+
delete sideEffects._resetRequested;
|
|
1924
|
+
const settingsPath = getUserSettingsPath();
|
|
1925
|
+
if (deleteSettings(settingsPath)) {
|
|
1926
|
+
addEntry(
|
|
1927
|
+
messageToHistoryEntry3(createSystemMessage3(`Deleted ${settingsPath}. Exiting...`))
|
|
1928
|
+
);
|
|
1929
|
+
} else {
|
|
1930
|
+
addEntry(messageToHistoryEntry3(createSystemMessage3("No user settings found.")));
|
|
1931
|
+
}
|
|
1932
|
+
requestShutdown("other", "Reset settings restart");
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
if (sideEffects._exitRequested) {
|
|
1936
|
+
delete sideEffects._exitRequested;
|
|
1937
|
+
requestShutdown("prompt_input_exit", "User requested exit");
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
if (sideEffects._triggerPluginTUI) {
|
|
1941
|
+
delete sideEffects._triggerPluginTUI;
|
|
1942
|
+
setShowPluginTUI(true);
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
if (sideEffects._triggerResumePicker) {
|
|
1946
|
+
delete sideEffects._triggerResumePicker;
|
|
1947
|
+
setShowSessionPicker(true);
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
if (sideEffects._sessionName) {
|
|
1951
|
+
const name = sideEffects._sessionName;
|
|
1952
|
+
delete sideEffects._sessionName;
|
|
1953
|
+
interactiveSession.setName(name);
|
|
1954
|
+
setSessionName(name);
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
},
|
|
1958
|
+
[interactiveSession, baseHandleSubmit, addEntry, requestShutdown, setSessionName]
|
|
1959
|
+
);
|
|
1960
|
+
const handleModelConfirm = useCallback3(
|
|
1961
|
+
(index) => {
|
|
1962
|
+
const modelId = pendingModelChangeRef.current;
|
|
1963
|
+
setPendingModelId(null);
|
|
1964
|
+
pendingModelChangeRef.current = null;
|
|
1965
|
+
if (index === 0 && modelId) {
|
|
1966
|
+
try {
|
|
1967
|
+
const settingsPath = getUserSettingsPath();
|
|
1968
|
+
updateModelInSettings(settingsPath, modelId);
|
|
1969
|
+
addEntry(
|
|
1970
|
+
messageToHistoryEntry3(
|
|
1971
|
+
createSystemMessage3(`Model changed to ${getModelName(modelId)}. Restarting...`)
|
|
1972
|
+
)
|
|
1973
|
+
);
|
|
1974
|
+
requestShutdown("other", "Model change restart");
|
|
1975
|
+
} catch (err) {
|
|
1976
|
+
addEntry(
|
|
1977
|
+
messageToHistoryEntry3(
|
|
1978
|
+
createSystemMessage3(`Failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
1979
|
+
)
|
|
1980
|
+
);
|
|
1981
|
+
}
|
|
1982
|
+
} else {
|
|
1983
|
+
addEntry(messageToHistoryEntry3(createSystemMessage3("Model change cancelled.")));
|
|
1984
|
+
}
|
|
1985
|
+
},
|
|
1986
|
+
[addEntry, requestShutdown]
|
|
1987
|
+
);
|
|
1988
|
+
const handleProviderConfirm = useCallback3(
|
|
1989
|
+
(index) => {
|
|
1990
|
+
const profile = pendingProviderProfileRef.current;
|
|
1991
|
+
setPendingProviderProfile(null);
|
|
1992
|
+
pendingProviderProfileRef.current = null;
|
|
1993
|
+
if (index === 0 && profile) {
|
|
1994
|
+
try {
|
|
1995
|
+
const settingsPath = getUserSettingsPath();
|
|
1996
|
+
applyProviderSwitch(settingsPath, profile, {
|
|
1997
|
+
knownProviders: readMergedProviderSettings(cwd).providers
|
|
1998
|
+
});
|
|
1999
|
+
addEntry(
|
|
2000
|
+
messageToHistoryEntry3(
|
|
2001
|
+
createSystemMessage3(`Provider changed to ${profile}. Restarting...`)
|
|
2002
|
+
)
|
|
2003
|
+
);
|
|
2004
|
+
requestShutdown("other", "Provider change restart");
|
|
2005
|
+
} catch (err) {
|
|
2006
|
+
addEntry(
|
|
2007
|
+
messageToHistoryEntry3(
|
|
2008
|
+
createSystemMessage3(`Failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
2009
|
+
)
|
|
2010
|
+
);
|
|
2011
|
+
}
|
|
2012
|
+
} else {
|
|
2013
|
+
addEntry(messageToHistoryEntry3(createSystemMessage3("Provider change cancelled.")));
|
|
2014
|
+
}
|
|
2015
|
+
},
|
|
2016
|
+
[cwd, addEntry, requestShutdown]
|
|
2017
|
+
);
|
|
2018
|
+
const handleProviderSetupSubmit = useCallback3(
|
|
2019
|
+
(input) => {
|
|
2020
|
+
setPendingProviderSetupType(null);
|
|
2021
|
+
try {
|
|
2022
|
+
const settingsPath = getUserSettingsPath();
|
|
2023
|
+
applyProviderConfiguration(settingsPath, input, { providerDefinitions });
|
|
2024
|
+
addEntry(
|
|
2025
|
+
messageToHistoryEntry3(
|
|
2026
|
+
createSystemMessage3(`Provider ${input.profile} configured. Restarting...`)
|
|
2027
|
+
)
|
|
2028
|
+
);
|
|
2029
|
+
requestShutdown("other", "Provider setup restart");
|
|
2030
|
+
} catch (err) {
|
|
2031
|
+
addEntry(
|
|
2032
|
+
messageToHistoryEntry3(
|
|
2033
|
+
createSystemMessage3(`Failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
2034
|
+
)
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
},
|
|
2038
|
+
[addEntry, requestShutdown, providerDefinitions]
|
|
2039
|
+
);
|
|
2040
|
+
const handleProviderSetupCancel = useCallback3(() => {
|
|
2041
|
+
setPendingProviderSetupType(null);
|
|
2042
|
+
addEntry(messageToHistoryEntry3(createSystemMessage3("Provider setup cancelled.")));
|
|
2043
|
+
}, [addEntry]);
|
|
2044
|
+
return {
|
|
2045
|
+
handleSubmit,
|
|
2046
|
+
pendingModelId,
|
|
2047
|
+
pendingProviderProfile,
|
|
2048
|
+
pendingProviderSetupType,
|
|
2049
|
+
showPluginTUI,
|
|
2050
|
+
showSessionPicker,
|
|
2051
|
+
setPendingModelId,
|
|
2052
|
+
setShowPluginTUI,
|
|
2053
|
+
setShowSessionPicker,
|
|
2054
|
+
handleModelConfirm,
|
|
2055
|
+
handleProviderConfirm,
|
|
2056
|
+
handleProviderSetupSubmit,
|
|
2057
|
+
handleProviderSetupCancel
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// src/ui/MessageList.tsx
|
|
2062
|
+
import React from "react";
|
|
2063
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
2064
|
+
import { isToolMessage, isAssistantMessage } from "@robota-sdk/agent-core";
|
|
2065
|
+
|
|
2066
|
+
// src/ui/render-markdown.ts
|
|
2067
|
+
import { marked } from "marked";
|
|
2068
|
+
import TerminalRenderer from "marked-terminal";
|
|
2069
|
+
marked.setOptions({
|
|
2070
|
+
renderer: new TerminalRenderer()
|
|
2071
|
+
});
|
|
2072
|
+
function renderMarkdown(md) {
|
|
2073
|
+
const result = marked.parse(md);
|
|
2074
|
+
return typeof result === "string" ? result.trimEnd() : md;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// src/ui/DiffBlock.tsx
|
|
2078
|
+
import { Box, Text } from "ink";
|
|
2079
|
+
import { jsxs } from "react/jsx-runtime";
|
|
2080
|
+
var MAX_DIFF_LINES = 12;
|
|
2081
|
+
var TRUNCATED_SHOW = 10;
|
|
2082
|
+
function DiffBlock({ file, lines }) {
|
|
2083
|
+
const truncated = lines.length > MAX_DIFF_LINES;
|
|
2084
|
+
const visible = truncated ? lines.slice(0, TRUNCATED_SHOW) : lines;
|
|
2085
|
+
const remaining = lines.length - TRUNCATED_SHOW;
|
|
2086
|
+
const maxLineNum = Math.max(...visible.map((l) => l.lineNumber), 0);
|
|
2087
|
+
const numWidth = String(maxLineNum).length;
|
|
2088
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginLeft: 4, children: [
|
|
2089
|
+
file && /* @__PURE__ */ jsxs(Text, { color: "white", dimColor: true, children: [
|
|
2090
|
+
"\u2502 ",
|
|
2091
|
+
file
|
|
2092
|
+
] }),
|
|
2093
|
+
visible.map((line, i) => {
|
|
2094
|
+
const lineNum = String(line.lineNumber).padStart(numWidth, " ");
|
|
2095
|
+
if (line.type === "context") {
|
|
2096
|
+
return /* @__PURE__ */ jsxs(Text, { color: "white", dimColor: true, children: [
|
|
2097
|
+
"\u2502 ",
|
|
2098
|
+
lineNum,
|
|
2099
|
+
" ",
|
|
2100
|
+
line.text
|
|
2101
|
+
] }, i);
|
|
2102
|
+
}
|
|
2103
|
+
const prefix = line.type === "remove" ? "-" : "+";
|
|
2104
|
+
const bgColor = line.type === "remove" ? "#5c1a1a" : "#1a3d1a";
|
|
2105
|
+
const fgColor = line.type === "remove" ? "#ff9999" : "#99ff99";
|
|
2106
|
+
return /* @__PURE__ */ jsxs(Text, { color: fgColor, backgroundColor: bgColor, children: [
|
|
2107
|
+
"\u2502 ",
|
|
2108
|
+
lineNum,
|
|
2109
|
+
" ",
|
|
2110
|
+
prefix,
|
|
2111
|
+
" ",
|
|
2112
|
+
line.text
|
|
2113
|
+
] }, i);
|
|
2114
|
+
}),
|
|
2115
|
+
truncated && /* @__PURE__ */ jsxs(Text, { color: "white", dimColor: true, children: [
|
|
2116
|
+
"\u2502 ... and ",
|
|
2117
|
+
remaining,
|
|
2118
|
+
" more lines"
|
|
2119
|
+
] })
|
|
2120
|
+
] });
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
// src/ui/MessageList.tsx
|
|
2124
|
+
import { Fragment, jsx, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
2125
|
+
function RoleLabel({ role }) {
|
|
2126
|
+
switch (role) {
|
|
2127
|
+
case "user":
|
|
2128
|
+
return /* @__PURE__ */ jsxs2(Text2, { color: "green", bold: true, children: [
|
|
2129
|
+
"You:",
|
|
2130
|
+
" "
|
|
2131
|
+
] });
|
|
2132
|
+
case "assistant":
|
|
2133
|
+
return /* @__PURE__ */ jsxs2(Text2, { color: "cyan", bold: true, children: [
|
|
2134
|
+
"Robota:",
|
|
2135
|
+
" "
|
|
2136
|
+
] });
|
|
2137
|
+
case "system":
|
|
2138
|
+
return /* @__PURE__ */ jsxs2(Text2, { color: "yellow", bold: true, children: [
|
|
2139
|
+
"System:",
|
|
2140
|
+
" "
|
|
2141
|
+
] });
|
|
2142
|
+
case "tool":
|
|
2143
|
+
return /* @__PURE__ */ jsxs2(Text2, { color: "white", bold: true, children: [
|
|
2144
|
+
"Tool:",
|
|
2145
|
+
" "
|
|
2146
|
+
] });
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
function ToolMessage({ message }) {
|
|
2150
|
+
if (!isToolMessage(message)) {
|
|
2151
|
+
return /* @__PURE__ */ jsx(Fragment, {});
|
|
2152
|
+
}
|
|
2153
|
+
const toolName = message.name;
|
|
2154
|
+
const content = message.content;
|
|
2155
|
+
let summaries = null;
|
|
2156
|
+
try {
|
|
2157
|
+
const parsed = JSON.parse(content);
|
|
2158
|
+
if (Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0].line === "string") {
|
|
2159
|
+
summaries = parsed;
|
|
2160
|
+
}
|
|
2161
|
+
} catch {
|
|
2162
|
+
}
|
|
2163
|
+
if (summaries) {
|
|
2164
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginBottom: 1, children: [
|
|
2165
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
2166
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "white", bold: true, children: [
|
|
2167
|
+
"Tool:",
|
|
2168
|
+
" "
|
|
2169
|
+
] }),
|
|
2170
|
+
toolName && /* @__PURE__ */ jsxs2(Text2, { color: "white", dimColor: true, children: [
|
|
2171
|
+
"[",
|
|
2172
|
+
toolName,
|
|
2173
|
+
"]"
|
|
2174
|
+
] })
|
|
2175
|
+
] }),
|
|
2176
|
+
/* @__PURE__ */ jsx(Text2, { children: " " }),
|
|
2177
|
+
summaries.map((s, i) => /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
2178
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "green", children: [
|
|
2179
|
+
" ",
|
|
2180
|
+
"\u2713",
|
|
2181
|
+
" ",
|
|
2182
|
+
s.line
|
|
2183
|
+
] }),
|
|
2184
|
+
s.diffLines && s.diffLines.length > 0 && /* @__PURE__ */ jsx(DiffBlock, { file: s.diffFile, lines: s.diffLines })
|
|
2185
|
+
] }, i))
|
|
2186
|
+
] });
|
|
2187
|
+
}
|
|
2188
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
2189
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginBottom: 1, children: [
|
|
2190
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
2191
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "white", bold: true, children: [
|
|
2192
|
+
"Tool:",
|
|
2193
|
+
" "
|
|
2194
|
+
] }),
|
|
2195
|
+
toolName && /* @__PURE__ */ jsxs2(Text2, { color: "white", dimColor: true, children: [
|
|
2196
|
+
"[",
|
|
2197
|
+
toolName,
|
|
2198
|
+
"]"
|
|
2199
|
+
] })
|
|
2200
|
+
] }),
|
|
2201
|
+
/* @__PURE__ */ jsx(Text2, { children: " " }),
|
|
2202
|
+
lines.map((line, i) => /* @__PURE__ */ jsxs2(Text2, { color: "green", children: [
|
|
2203
|
+
" ",
|
|
2204
|
+
"\u2713",
|
|
2205
|
+
" ",
|
|
2206
|
+
line
|
|
2207
|
+
] }, i))
|
|
2208
|
+
] });
|
|
2209
|
+
}
|
|
2210
|
+
var MessageItem = React.memo(function MessageItem2({
|
|
2211
|
+
message
|
|
2212
|
+
}) {
|
|
2213
|
+
if (isToolMessage(message)) {
|
|
2214
|
+
return /* @__PURE__ */ jsx(ToolMessage, { message });
|
|
2215
|
+
}
|
|
2216
|
+
const content = message.content ?? "";
|
|
2217
|
+
const isInterrupted = message.state === "interrupted";
|
|
2218
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginBottom: 1, children: [
|
|
2219
|
+
/* @__PURE__ */ jsx(Box2, { children: /* @__PURE__ */ jsx(RoleLabel, { role: message.role }) }),
|
|
2220
|
+
/* @__PURE__ */ jsx(Text2, { children: " " }),
|
|
2221
|
+
/* @__PURE__ */ jsx(Box2, { marginLeft: 2, children: /* @__PURE__ */ jsx(Text2, { wrap: "wrap", children: isAssistantMessage(message) ? renderMarkdown(content + (isInterrupted ? "\n\n_(interrupted)_" : "")) : content }) })
|
|
2222
|
+
] });
|
|
2223
|
+
});
|
|
2224
|
+
function ToolSummaryEntry({ entry }) {
|
|
2225
|
+
const data = entry.data;
|
|
2226
|
+
const lines = data?.summary?.split("\n") ?? [];
|
|
2227
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginBottom: 1, children: [
|
|
2228
|
+
/* @__PURE__ */ jsx(Box2, { children: /* @__PURE__ */ jsxs2(Text2, { color: "white", bold: true, children: [
|
|
2229
|
+
"Tool:",
|
|
2230
|
+
" "
|
|
2231
|
+
] }) }),
|
|
2232
|
+
/* @__PURE__ */ jsx(Text2, { children: " " }),
|
|
2233
|
+
lines.map((line, i) => /* @__PURE__ */ jsxs2(Text2, { color: "green", children: [
|
|
2234
|
+
" ",
|
|
2235
|
+
line
|
|
2236
|
+
] }, i))
|
|
2237
|
+
] });
|
|
2238
|
+
}
|
|
2239
|
+
function EventEntry({ entry }) {
|
|
2240
|
+
const eventData = entry.data;
|
|
2241
|
+
const eventMessage = typeof eventData?.message === "string" ? eventData.message : typeof eventData?.content === "string" ? eventData.content : entry.type;
|
|
2242
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginBottom: 1, children: [
|
|
2243
|
+
/* @__PURE__ */ jsx(Box2, { children: /* @__PURE__ */ jsxs2(Text2, { color: "yellow", bold: true, children: [
|
|
2244
|
+
"System:",
|
|
2245
|
+
" "
|
|
2246
|
+
] }) }),
|
|
2247
|
+
/* @__PURE__ */ jsx(Text2, { children: " " }),
|
|
2248
|
+
/* @__PURE__ */ jsx(Box2, { marginLeft: 2, children: /* @__PURE__ */ jsx(Text2, { wrap: "wrap", children: eventMessage }) })
|
|
2249
|
+
] });
|
|
2250
|
+
}
|
|
2251
|
+
function EntryItem({ entry }) {
|
|
2252
|
+
if (entry.category === "chat") {
|
|
2253
|
+
const message = entry.data;
|
|
2254
|
+
return /* @__PURE__ */ jsx(MessageItem, { message });
|
|
2255
|
+
}
|
|
2256
|
+
if (entry.type === "tool-summary") {
|
|
2257
|
+
return /* @__PURE__ */ jsx(ToolSummaryEntry, { entry });
|
|
2258
|
+
}
|
|
2259
|
+
if (entry.type === "tool-start" || entry.type === "tool-end") {
|
|
2260
|
+
return /* @__PURE__ */ jsx(Fragment, {});
|
|
2261
|
+
}
|
|
2262
|
+
return /* @__PURE__ */ jsx(EventEntry, { entry });
|
|
2263
|
+
}
|
|
2264
|
+
function MessageList({ history }) {
|
|
2265
|
+
return /* @__PURE__ */ jsx(Box2, { flexDirection: "column", children: history.map((entry) => /* @__PURE__ */ jsx(EntryItem, { entry }, entry.id)) });
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
// src/ui/StatusBar.tsx
|
|
2269
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
2270
|
+
import { formatTokenCount } from "@robota-sdk/agent-core";
|
|
2271
|
+
import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
2272
|
+
var CONTEXT_YELLOW_THRESHOLD = 70;
|
|
2273
|
+
var CONTEXT_RED_THRESHOLD = 90;
|
|
2274
|
+
function getContextColor(percentage) {
|
|
2275
|
+
if (percentage >= CONTEXT_RED_THRESHOLD) return "red";
|
|
2276
|
+
if (percentage >= CONTEXT_YELLOW_THRESHOLD) return "yellow";
|
|
2277
|
+
return "green";
|
|
2278
|
+
}
|
|
2279
|
+
function StatusBar({
|
|
2280
|
+
permissionMode,
|
|
2281
|
+
modelName,
|
|
2282
|
+
sessionId: _sessionId,
|
|
2283
|
+
messageCount,
|
|
2284
|
+
isThinking,
|
|
2285
|
+
contextPercentage,
|
|
2286
|
+
contextUsedTokens,
|
|
2287
|
+
contextMaxTokens,
|
|
2288
|
+
sessionName
|
|
2289
|
+
}) {
|
|
2290
|
+
const contextColor = getContextColor(contextPercentage);
|
|
2291
|
+
return /* @__PURE__ */ jsxs3(
|
|
2292
|
+
Box3,
|
|
2293
|
+
{
|
|
2294
|
+
borderStyle: "single",
|
|
2295
|
+
borderColor: "gray",
|
|
2296
|
+
paddingLeft: 1,
|
|
2297
|
+
paddingRight: 1,
|
|
2298
|
+
justifyContent: "space-between",
|
|
2299
|
+
children: [
|
|
2300
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
2301
|
+
/* @__PURE__ */ jsx2(Text3, { color: "cyan", bold: true, children: "Mode:" }),
|
|
2302
|
+
" ",
|
|
2303
|
+
/* @__PURE__ */ jsx2(Text3, { children: permissionMode }),
|
|
2304
|
+
sessionName && /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
2305
|
+
" | ",
|
|
2306
|
+
/* @__PURE__ */ jsx2(Text3, { color: "magenta", children: sessionName })
|
|
2307
|
+
] }),
|
|
2308
|
+
" | ",
|
|
2309
|
+
/* @__PURE__ */ jsx2(Text3, { dimColor: true, children: modelName }),
|
|
2310
|
+
" | ",
|
|
2311
|
+
/* @__PURE__ */ jsxs3(Text3, { color: contextColor, children: [
|
|
2312
|
+
"Context: ",
|
|
2313
|
+
Math.round(contextPercentage),
|
|
2314
|
+
"% (",
|
|
2315
|
+
formatTokenCount(contextUsedTokens),
|
|
2316
|
+
"/",
|
|
2317
|
+
formatTokenCount(contextMaxTokens),
|
|
2318
|
+
")"
|
|
2319
|
+
] })
|
|
2320
|
+
] }),
|
|
2321
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
2322
|
+
isThinking && /* @__PURE__ */ jsx2(Text3, { color: "yellow", children: "Thinking... " }),
|
|
2323
|
+
/* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
2324
|
+
"msgs: ",
|
|
2325
|
+
messageCount
|
|
2326
|
+
] })
|
|
2327
|
+
] })
|
|
2328
|
+
]
|
|
2329
|
+
}
|
|
2330
|
+
);
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
// src/ui/InputArea.tsx
|
|
2334
|
+
import { useState as useState6, useCallback as useCallback4, useRef as useRef4 } from "react";
|
|
2335
|
+
import { Box as Box5, Text as Text7, useInput as useInput2, useStdout } from "ink";
|
|
2336
|
+
|
|
2337
|
+
// src/ui/CjkTextInput.tsx
|
|
2338
|
+
import { useRef as useRef3, useState as useState3 } from "react";
|
|
2339
|
+
import { Text as Text4, useInput } from "ink";
|
|
2340
|
+
import chalk from "chalk";
|
|
2341
|
+
|
|
2342
|
+
// src/ui/flows/cjk-text-input-flow.ts
|
|
2343
|
+
import stringWidth from "string-width";
|
|
2344
|
+
var PASTE_START = "[200~";
|
|
2345
|
+
var PASTE_END = "[201~";
|
|
2346
|
+
var LAST_ASCII_CONTROL_CODE = 31;
|
|
2347
|
+
var DELETE_CONTROL_CODE = 127;
|
|
2348
|
+
function createCjkTextInputFlowState(value) {
|
|
2349
|
+
return { value, cursor: value.length, isPasting: false, pasteBuffer: "" };
|
|
2350
|
+
}
|
|
2351
|
+
function syncCjkTextInputFlowState(state, value, cursorHint) {
|
|
2352
|
+
if (value === state.value) {
|
|
2353
|
+
return state;
|
|
2354
|
+
}
|
|
2355
|
+
return {
|
|
2356
|
+
...state,
|
|
2357
|
+
value,
|
|
2358
|
+
cursor: cursorHint != null ? Math.min(cursorHint, value.length) : value.length
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
function applyCjkTextInput(state, input, key, options) {
|
|
2362
|
+
const pasteResult = applyPasteBoundaryInput(state, input, options);
|
|
2363
|
+
if (pasteResult !== void 0) return pasteResult;
|
|
2364
|
+
const controlResult = applyControlInput(state, input, key, options);
|
|
2365
|
+
if (controlResult !== void 0) return controlResult;
|
|
2366
|
+
const cursorResult = applyCursorInput(state, key, options.availableWidth);
|
|
2367
|
+
if (cursorResult !== void 0) return cursorResult;
|
|
2368
|
+
return insertPrintableInput(state, input);
|
|
2369
|
+
}
|
|
2370
|
+
function applyPasteBoundaryInput(state, input, options) {
|
|
2371
|
+
if (input === PASTE_START || input.startsWith(PASTE_START)) {
|
|
2372
|
+
return startBracketedPaste(state, input);
|
|
2373
|
+
}
|
|
2374
|
+
if (state.isPasting) {
|
|
2375
|
+
return continueBracketedPaste(state, input, options);
|
|
2376
|
+
}
|
|
2377
|
+
return void 0;
|
|
2378
|
+
}
|
|
2379
|
+
function applyControlInput(state, input, key, options) {
|
|
2380
|
+
if (key.ctrl === true && input === "c" || key.tab === true) {
|
|
2381
|
+
return { state, effect: { type: "none" } };
|
|
2382
|
+
}
|
|
2383
|
+
if (key.return === true) {
|
|
2384
|
+
return { state, effect: { type: "submit", value: state.value } };
|
|
2385
|
+
}
|
|
2386
|
+
if (input.length > 1 && (input.includes("\n") || input.includes("\r")) && options.canPaste) {
|
|
2387
|
+
return {
|
|
2388
|
+
state,
|
|
2389
|
+
effect: { type: "paste", text: input.replace(/\r\n?/g, "\n"), cursor: state.cursor }
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
2392
|
+
return void 0;
|
|
2393
|
+
}
|
|
2394
|
+
function applyCursorInput(state, key, availableWidth) {
|
|
2395
|
+
if (key.upArrow === true || key.downArrow === true) {
|
|
2396
|
+
return moveCursorVertically(state, key.upArrow === true ? "up" : "down", availableWidth);
|
|
2397
|
+
}
|
|
2398
|
+
if (key.leftArrow === true) {
|
|
2399
|
+
return moveCursorHorizontally(state, "left");
|
|
2400
|
+
}
|
|
2401
|
+
if (key.rightArrow === true) {
|
|
2402
|
+
return moveCursorHorizontally(state, "right");
|
|
2403
|
+
}
|
|
2404
|
+
if (key.backspace === true || key.delete === true) {
|
|
2405
|
+
return deleteBeforeCursor(state);
|
|
2406
|
+
}
|
|
2407
|
+
return void 0;
|
|
2408
|
+
}
|
|
2409
|
+
function filterPrintable(input) {
|
|
2410
|
+
if (!input || input.length === 0) return "";
|
|
2411
|
+
let output = "";
|
|
2412
|
+
for (const char of input) {
|
|
2413
|
+
const code = char.charCodeAt(0);
|
|
2414
|
+
if (code > LAST_ASCII_CONTROL_CODE && code !== DELETE_CONTROL_CODE) {
|
|
2415
|
+
output += char;
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
return output;
|
|
2419
|
+
}
|
|
2420
|
+
function insertAtCursor(value, cursor, input) {
|
|
2421
|
+
const next = value.slice(0, cursor) + input + value.slice(cursor);
|
|
2422
|
+
return { value: next, cursor: cursor + input.length };
|
|
2423
|
+
}
|
|
2424
|
+
function displayOffset(chars, charIndex, width) {
|
|
2425
|
+
let offset = 0;
|
|
2426
|
+
for (let i = 0; i < charIndex && i < chars.length; i++) {
|
|
2427
|
+
const w = stringWidth(chars[i]);
|
|
2428
|
+
const col = offset % width;
|
|
2429
|
+
if (col + w > width) offset += width - col;
|
|
2430
|
+
offset += w;
|
|
2431
|
+
}
|
|
2432
|
+
return offset;
|
|
2433
|
+
}
|
|
2434
|
+
function charIndexAtDisplayOffset(chars, target, width) {
|
|
2435
|
+
let offset = 0;
|
|
2436
|
+
for (let i = 0; i < chars.length; i++) {
|
|
2437
|
+
if (offset >= target) return i;
|
|
2438
|
+
const w = stringWidth(chars[i]);
|
|
2439
|
+
const col = offset % width;
|
|
2440
|
+
if (col + w > width) offset += width - col;
|
|
2441
|
+
offset += w;
|
|
2442
|
+
}
|
|
2443
|
+
return chars.length;
|
|
2444
|
+
}
|
|
2445
|
+
function startBracketedPaste(state, input) {
|
|
2446
|
+
return {
|
|
2447
|
+
state: { ...state, isPasting: true, pasteBuffer: input.slice(PASTE_START.length) },
|
|
2448
|
+
effect: { type: "none" }
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
function continueBracketedPaste(state, input, options) {
|
|
2452
|
+
if (input !== PASTE_END && !input.includes(PASTE_END)) {
|
|
2453
|
+
return {
|
|
2454
|
+
state: { ...state, pasteBuffer: state.pasteBuffer + input },
|
|
2455
|
+
effect: { type: "none" }
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
const beforeMarker = input.split(PASTE_END)[0] ?? "";
|
|
2459
|
+
const text = (state.pasteBuffer + beforeMarker).replace(/\r\n?/g, "\n");
|
|
2460
|
+
const nextState = { ...state, isPasting: false, pasteBuffer: "" };
|
|
2461
|
+
if (text.length === 0) {
|
|
2462
|
+
return { state: nextState, effect: { type: "none" } };
|
|
2463
|
+
}
|
|
2464
|
+
if (text.includes("\n") && options.canPaste) {
|
|
2465
|
+
return { state: nextState, effect: { type: "paste", text, cursor: state.cursor } };
|
|
2466
|
+
}
|
|
2467
|
+
return insertPrintableInput(nextState, text);
|
|
2468
|
+
}
|
|
2469
|
+
function moveCursorVertically(state, direction, availableWidth) {
|
|
2470
|
+
if (!availableWidth || availableWidth <= 0) {
|
|
2471
|
+
return { state, effect: { type: "none" } };
|
|
2472
|
+
}
|
|
2473
|
+
const chars = [...state.value];
|
|
2474
|
+
const offset = displayOffset(chars, state.cursor, availableWidth);
|
|
2475
|
+
const target = direction === "up" ? offset - availableWidth : offset + availableWidth;
|
|
2476
|
+
if (target < 0) {
|
|
2477
|
+
return { state, effect: { type: "none" } };
|
|
2478
|
+
}
|
|
2479
|
+
const cursor = charIndexAtDisplayOffset(chars, target, availableWidth);
|
|
2480
|
+
if (cursor === state.cursor) {
|
|
2481
|
+
return { state, effect: { type: "none" } };
|
|
2482
|
+
}
|
|
2483
|
+
return { state: { ...state, cursor }, effect: { type: "render" } };
|
|
2484
|
+
}
|
|
2485
|
+
function moveCursorHorizontally(state, direction) {
|
|
2486
|
+
if (direction === "left" && state.cursor > 0) {
|
|
2487
|
+
return { state: { ...state, cursor: state.cursor - 1 }, effect: { type: "render" } };
|
|
2488
|
+
}
|
|
2489
|
+
if (direction === "right" && state.cursor < state.value.length) {
|
|
2490
|
+
return { state: { ...state, cursor: state.cursor + 1 }, effect: { type: "render" } };
|
|
2491
|
+
}
|
|
2492
|
+
return { state, effect: { type: "none" } };
|
|
2493
|
+
}
|
|
2494
|
+
function deleteBeforeCursor(state) {
|
|
2495
|
+
if (state.cursor === 0) {
|
|
2496
|
+
return { state, effect: { type: "none" } };
|
|
2497
|
+
}
|
|
2498
|
+
const value = state.value.slice(0, state.cursor - 1) + state.value.slice(state.cursor);
|
|
2499
|
+
return {
|
|
2500
|
+
state: { ...state, value, cursor: state.cursor - 1 },
|
|
2501
|
+
effect: { type: "change", value }
|
|
2502
|
+
};
|
|
2503
|
+
}
|
|
2504
|
+
function insertPrintableInput(state, input) {
|
|
2505
|
+
const printable = filterPrintable(input);
|
|
2506
|
+
if (printable.length === 0) {
|
|
2507
|
+
return { state, effect: { type: "none" } };
|
|
2508
|
+
}
|
|
2509
|
+
const result = insertAtCursor(state.value, state.cursor, printable);
|
|
2510
|
+
return {
|
|
2511
|
+
state: { ...state, value: result.value, cursor: result.cursor },
|
|
2512
|
+
effect: { type: "change", value: result.value }
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
// src/ui/CjkTextInput.tsx
|
|
2517
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
2518
|
+
function CjkTextInput({
|
|
2519
|
+
value,
|
|
2520
|
+
onChange,
|
|
2521
|
+
onSubmit,
|
|
2522
|
+
onPaste,
|
|
2523
|
+
placeholder = "",
|
|
2524
|
+
focus = true,
|
|
2525
|
+
showCursor = true,
|
|
2526
|
+
availableWidth,
|
|
2527
|
+
cursorHint = null
|
|
2528
|
+
}) {
|
|
2529
|
+
const stateRef = useRef3(createCjkTextInputFlowState(value));
|
|
2530
|
+
const [, forceRender] = useState3(0);
|
|
2531
|
+
stateRef.current = syncCjkTextInputFlowState(stateRef.current, value, cursorHint);
|
|
2532
|
+
useInput(
|
|
2533
|
+
(input, key) => {
|
|
2534
|
+
try {
|
|
2535
|
+
const result = applyCjkTextInput(stateRef.current, input, key, {
|
|
2536
|
+
availableWidth,
|
|
2537
|
+
canPaste: onPaste !== void 0
|
|
2538
|
+
});
|
|
2539
|
+
stateRef.current = result.state;
|
|
2540
|
+
applyCjkTextInputEffect(result.effect, onChange, onSubmit, onPaste, forceRender);
|
|
2541
|
+
} catch {
|
|
2542
|
+
}
|
|
2543
|
+
},
|
|
2544
|
+
{ isActive: focus }
|
|
2545
|
+
);
|
|
2546
|
+
return /* @__PURE__ */ jsx3(Text4, { children: renderWithCursor(
|
|
2547
|
+
stateRef.current.value,
|
|
2548
|
+
stateRef.current.cursor,
|
|
2549
|
+
placeholder,
|
|
2550
|
+
showCursor && focus
|
|
2551
|
+
) });
|
|
2552
|
+
}
|
|
2553
|
+
function applyCjkTextInputEffect(effect, onChange, onSubmit, onPaste, forceRender) {
|
|
2554
|
+
if (effect.type === "change") {
|
|
2555
|
+
onChange(effect.value);
|
|
2556
|
+
} else if (effect.type === "submit") {
|
|
2557
|
+
onSubmit?.(effect.value);
|
|
2558
|
+
} else if (effect.type === "paste") {
|
|
2559
|
+
onPaste?.(effect.text, effect.cursor);
|
|
2560
|
+
} else if (effect.type === "render") {
|
|
2561
|
+
forceRender((n) => n + 1);
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
function renderWithCursor(value, cursorOffset, placeholder, showCursor) {
|
|
2565
|
+
if (!showCursor) {
|
|
2566
|
+
return value.length > 0 ? value : placeholder ? chalk.gray(placeholder) : "";
|
|
2567
|
+
}
|
|
2568
|
+
if (value.length === 0) {
|
|
2569
|
+
if (placeholder.length > 0) {
|
|
2570
|
+
return chalk.inverse(placeholder[0]) + chalk.gray(placeholder.slice(1));
|
|
2571
|
+
}
|
|
2572
|
+
return chalk.inverse(" ");
|
|
2573
|
+
}
|
|
2574
|
+
const chars = [...value];
|
|
2575
|
+
let rendered = "";
|
|
2576
|
+
for (let i = 0; i < chars.length; i++) {
|
|
2577
|
+
const char = chars[i] ?? "";
|
|
2578
|
+
rendered += i === cursorOffset ? chalk.inverse(char) : char;
|
|
2579
|
+
}
|
|
2580
|
+
if (cursorOffset >= chars.length) {
|
|
2581
|
+
rendered += chalk.inverse(" ");
|
|
2582
|
+
}
|
|
2583
|
+
return rendered;
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
// src/ui/WaveText.tsx
|
|
2587
|
+
import { useState as useState4, useEffect as useEffect2 } from "react";
|
|
2588
|
+
import { Text as Text5 } from "ink";
|
|
2589
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
2590
|
+
var WAVE_COLORS = ["#666666", "#888888", "#aaaaaa", "#888888"];
|
|
2591
|
+
var INTERVAL_MS = 400;
|
|
2592
|
+
var CHARS_PER_GROUP = 4;
|
|
2593
|
+
function WaveText({ text }) {
|
|
2594
|
+
const [tick, setTick] = useState4(0);
|
|
2595
|
+
useEffect2(() => {
|
|
2596
|
+
const timer = setInterval(() => {
|
|
2597
|
+
setTick((prev) => prev + 1);
|
|
2598
|
+
}, INTERVAL_MS);
|
|
2599
|
+
return () => clearInterval(timer);
|
|
2600
|
+
}, []);
|
|
2601
|
+
const chars = [...text];
|
|
2602
|
+
return /* @__PURE__ */ jsx4(Text5, { children: chars.map((char, i) => {
|
|
2603
|
+
const group = Math.floor(i / CHARS_PER_GROUP);
|
|
2604
|
+
const colorIndex = (tick + group) % WAVE_COLORS.length;
|
|
2605
|
+
return /* @__PURE__ */ jsx4(Text5, { color: WAVE_COLORS[colorIndex], children: char }, i);
|
|
2606
|
+
}) });
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
// src/ui/SlashAutocomplete.tsx
|
|
2610
|
+
import { Box as Box4, Text as Text6 } from "ink";
|
|
2611
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2612
|
+
var MAX_VISIBLE = 8;
|
|
2613
|
+
function CommandRow(props) {
|
|
2614
|
+
const { cmd, isSelected, showSlash } = props;
|
|
2615
|
+
const indicator = isSelected ? "\u25B8 " : " ";
|
|
2616
|
+
const nameColor = isSelected ? "cyan" : void 0;
|
|
2617
|
+
const dimmed = !isSelected;
|
|
2618
|
+
return /* @__PURE__ */ jsx5(Box4, { children: /* @__PURE__ */ jsxs4(Text6, { color: nameColor, dimColor: dimmed, children: [
|
|
2619
|
+
indicator,
|
|
2620
|
+
showSlash ? `/${cmd.name} ${cmd.description}` : cmd.description
|
|
2621
|
+
] }) });
|
|
2622
|
+
}
|
|
2623
|
+
function SlashAutocomplete({
|
|
2624
|
+
commands,
|
|
2625
|
+
selectedIndex,
|
|
2626
|
+
visible,
|
|
2627
|
+
isSubcommandMode
|
|
2628
|
+
}) {
|
|
2629
|
+
if (!visible || commands.length === 0) return null;
|
|
2630
|
+
const scrollOffset = computeScrollOffset(selectedIndex, commands.length);
|
|
2631
|
+
const visibleCommands = commands.slice(scrollOffset, scrollOffset + MAX_VISIBLE);
|
|
2632
|
+
return /* @__PURE__ */ jsx5(Box4, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, children: visibleCommands.map((cmd, i) => /* @__PURE__ */ jsx5(
|
|
2633
|
+
CommandRow,
|
|
2634
|
+
{
|
|
2635
|
+
cmd,
|
|
2636
|
+
isSelected: scrollOffset + i === selectedIndex,
|
|
2637
|
+
showSlash: !isSubcommandMode
|
|
2638
|
+
},
|
|
2639
|
+
cmd.name
|
|
2640
|
+
)) });
|
|
2641
|
+
}
|
|
2642
|
+
function computeScrollOffset(selectedIndex, total) {
|
|
2643
|
+
if (total <= MAX_VISIBLE) return 0;
|
|
2644
|
+
if (selectedIndex < MAX_VISIBLE) return 0;
|
|
2645
|
+
const maxOffset = total - MAX_VISIBLE;
|
|
2646
|
+
return Math.min(selectedIndex - MAX_VISIBLE + 1, maxOffset);
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
// src/utils/paste-labels.ts
|
|
2650
|
+
var PASTE_LABEL_RE = /\[Pasted text #(\d+)(?: \+\d+ lines)?\]/g;
|
|
2651
|
+
function expandPasteLabels(text, store) {
|
|
2652
|
+
return text.replace(PASTE_LABEL_RE, (_, id) => store.get(Number(id)) ?? "");
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
// src/ui/hooks/useAutocomplete.ts
|
|
2656
|
+
import React4, { useState as useState5, useMemo as useMemo2 } from "react";
|
|
2657
|
+
function parseSlashInput(value) {
|
|
2658
|
+
if (!value.startsWith("/")) return { isSlash: false, parentCommand: "", filter: "" };
|
|
2659
|
+
const afterSlash = value.slice(1);
|
|
2660
|
+
const spaceIndex = afterSlash.indexOf(" ");
|
|
2661
|
+
if (spaceIndex === -1) return { isSlash: true, parentCommand: "", filter: afterSlash };
|
|
2662
|
+
const parent = afterSlash.slice(0, spaceIndex);
|
|
2663
|
+
const rest = afterSlash.slice(spaceIndex + 1);
|
|
2664
|
+
return { isSlash: true, parentCommand: parent, filter: rest };
|
|
2665
|
+
}
|
|
2666
|
+
function useAutocomplete(value, registry) {
|
|
2667
|
+
const [selectedIndex, setSelectedIndex] = useState5(0);
|
|
2668
|
+
const [dismissed, setDismissed] = useState5(false);
|
|
2669
|
+
const prevValueRef = React4.useRef(value);
|
|
2670
|
+
if (prevValueRef.current !== value) {
|
|
2671
|
+
prevValueRef.current = value;
|
|
2672
|
+
if (dismissed) setDismissed(false);
|
|
2673
|
+
}
|
|
2674
|
+
const parsed = parseSlashInput(value);
|
|
2675
|
+
const isSubcommandMode = parsed.isSlash && parsed.parentCommand.length > 0;
|
|
2676
|
+
const filteredCommands = useMemo2(() => {
|
|
2677
|
+
if (!registry || !parsed.isSlash || dismissed) return [];
|
|
2678
|
+
if (isSubcommandMode) {
|
|
2679
|
+
const subs = registry.getSubcommands(parsed.parentCommand);
|
|
2680
|
+
if (subs.length === 0) return [];
|
|
2681
|
+
if (!parsed.filter) return subs;
|
|
2682
|
+
const lower = parsed.filter.toLowerCase();
|
|
2683
|
+
return subs.filter((c) => c.name.toLowerCase().startsWith(lower));
|
|
2684
|
+
}
|
|
2685
|
+
return registry.getCommands(parsed.filter);
|
|
2686
|
+
}, [registry, parsed.isSlash, parsed.parentCommand, parsed.filter, dismissed, isSubcommandMode]);
|
|
2687
|
+
const showPopup = parsed.isSlash && filteredCommands.length > 0 && !dismissed;
|
|
2688
|
+
if (selectedIndex >= filteredCommands.length && filteredCommands.length > 0) {
|
|
2689
|
+
setSelectedIndex(filteredCommands.length - 1);
|
|
2690
|
+
}
|
|
2691
|
+
return {
|
|
2692
|
+
showPopup,
|
|
2693
|
+
filteredCommands,
|
|
2694
|
+
selectedIndex,
|
|
2695
|
+
setSelectedIndex,
|
|
2696
|
+
isSubcommandMode,
|
|
2697
|
+
setShowPopup: (val) => {
|
|
2698
|
+
if (typeof val === "function") {
|
|
2699
|
+
setDismissed((prev) => {
|
|
2700
|
+
const nextVal = val(!prev);
|
|
2701
|
+
return !nextVal;
|
|
2702
|
+
});
|
|
2703
|
+
} else {
|
|
2704
|
+
setDismissed(!val);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
};
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
// src/ui/flows/input-area-flow.ts
|
|
2711
|
+
function getAutocompletePopupAction(key) {
|
|
2712
|
+
if (key.upArrow === true) return "previous";
|
|
2713
|
+
if (key.downArrow === true) return "next";
|
|
2714
|
+
if (key.escape === true) return "close";
|
|
2715
|
+
if (key.tab === true) return "complete";
|
|
2716
|
+
return void 0;
|
|
2717
|
+
}
|
|
2718
|
+
function getPendingPromptInputAction(key) {
|
|
2719
|
+
if (key.backspace === true || key.delete === true) {
|
|
2720
|
+
return "cancelQueue";
|
|
2721
|
+
}
|
|
2722
|
+
return void 0;
|
|
2723
|
+
}
|
|
2724
|
+
function moveAutocompleteSelection(selectedIndex, commandCount, direction) {
|
|
2725
|
+
if (commandCount === 0) return 0;
|
|
2726
|
+
if (direction === "previous") {
|
|
2727
|
+
return selectedIndex > 0 ? selectedIndex - 1 : commandCount - 1;
|
|
2728
|
+
}
|
|
2729
|
+
return selectedIndex < commandCount - 1 ? selectedIndex + 1 : 0;
|
|
2730
|
+
}
|
|
2731
|
+
function resolveTabCompletion(value, command) {
|
|
2732
|
+
const parsed = parseSlashInput(value);
|
|
2733
|
+
if (parsed.parentCommand) {
|
|
2734
|
+
return { type: "insert", value: `/${parsed.parentCommand} ${command.name} ` };
|
|
2735
|
+
}
|
|
2736
|
+
if (command.subcommands && command.subcommands.length > 0) {
|
|
2737
|
+
return { type: "insert", value: `/${command.name} `, selectedIndex: 0 };
|
|
2738
|
+
}
|
|
2739
|
+
return { type: "insert", value: `/${command.name} ` };
|
|
2740
|
+
}
|
|
2741
|
+
function resolveEnterCommandSelection(value, command) {
|
|
2742
|
+
const parsed = parseSlashInput(value);
|
|
2743
|
+
if (parsed.parentCommand) {
|
|
2744
|
+
return { type: "submit", value: `/${parsed.parentCommand} ${command.name}` };
|
|
2745
|
+
}
|
|
2746
|
+
if (command.subcommands && command.subcommands.length > 0) {
|
|
2747
|
+
return { type: "insert", value: `/${command.name} `, selectedIndex: 0 };
|
|
2748
|
+
}
|
|
2749
|
+
return { type: "submit", value: `/${command.name}` };
|
|
2750
|
+
}
|
|
2751
|
+
function createPasteLabelChange(value, cursorPosition, pasteId, text) {
|
|
2752
|
+
const lineCount = text.split("\n").length;
|
|
2753
|
+
const label = `[Pasted text #${pasteId} +${lineCount} lines]`;
|
|
2754
|
+
return {
|
|
2755
|
+
value: value.slice(0, cursorPosition) + label + value.slice(cursorPosition),
|
|
2756
|
+
cursorHint: cursorPosition + label.length,
|
|
2757
|
+
label,
|
|
2758
|
+
lineCount
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
function shouldSubmitInput(text) {
|
|
2762
|
+
return text.trim().length > 0;
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
// src/ui/InputArea.tsx
|
|
2766
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2767
|
+
var BORDER_HORIZONTAL = 2;
|
|
2768
|
+
var PADDING_LEFT = 1;
|
|
2769
|
+
var PROMPT_WIDTH = 2;
|
|
2770
|
+
var INPUT_AREA_OVERHEAD = BORDER_HORIZONTAL + PADDING_LEFT + PROMPT_WIDTH;
|
|
2771
|
+
function InputArea({
|
|
2772
|
+
onSubmit,
|
|
2773
|
+
onCancelQueue,
|
|
2774
|
+
isDisabled,
|
|
2775
|
+
isAborting,
|
|
2776
|
+
pendingPrompt,
|
|
2777
|
+
registry,
|
|
2778
|
+
sessionName
|
|
2779
|
+
}) {
|
|
2780
|
+
const [value, setValue] = useState6("");
|
|
2781
|
+
const [cursorHint, setCursorHint] = useState6(null);
|
|
2782
|
+
const pasteStore = useRef4(/* @__PURE__ */ new Map());
|
|
2783
|
+
const { stdout } = useStdout();
|
|
2784
|
+
const terminalColumns = stdout?.columns ?? 80;
|
|
2785
|
+
const availableWidth = Math.max(1, terminalColumns - INPUT_AREA_OVERHEAD);
|
|
2786
|
+
const pasteIdRef = useRef4(0);
|
|
2787
|
+
const {
|
|
2788
|
+
showPopup,
|
|
2789
|
+
filteredCommands,
|
|
2790
|
+
selectedIndex,
|
|
2791
|
+
setSelectedIndex,
|
|
2792
|
+
isSubcommandMode,
|
|
2793
|
+
setShowPopup
|
|
2794
|
+
} = useAutocomplete(value, registry);
|
|
2795
|
+
const handlePaste = useCallback4((text, cursorPosition) => {
|
|
2796
|
+
pasteIdRef.current += 1;
|
|
2797
|
+
const id = pasteIdRef.current;
|
|
2798
|
+
pasteStore.current.set(id, text);
|
|
2799
|
+
setValue((prev) => {
|
|
2800
|
+
const change = createPasteLabelChange(prev, cursorPosition, id, text);
|
|
2801
|
+
setCursorHint(change.cursorHint);
|
|
2802
|
+
return change.value;
|
|
2803
|
+
});
|
|
2804
|
+
}, []);
|
|
2805
|
+
const tabCompleteCommand = useCallback4(
|
|
2806
|
+
(cmd) => {
|
|
2807
|
+
const result = resolveTabCompletion(value, cmd);
|
|
2808
|
+
if (result.type === "insert") {
|
|
2809
|
+
setValue(result.value);
|
|
2810
|
+
if (result.selectedIndex !== void 0) {
|
|
2811
|
+
setSelectedIndex(result.selectedIndex);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
},
|
|
2815
|
+
[value, setSelectedIndex]
|
|
2816
|
+
);
|
|
2817
|
+
const enterSelectCommand = useCallback4(
|
|
2818
|
+
(cmd) => {
|
|
2819
|
+
const result = resolveEnterCommandSelection(value, cmd);
|
|
2820
|
+
if (result.type === "insert") {
|
|
2821
|
+
setValue(result.value);
|
|
2822
|
+
if (result.selectedIndex !== void 0) {
|
|
2823
|
+
setSelectedIndex(result.selectedIndex);
|
|
2824
|
+
}
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
setValue("");
|
|
2828
|
+
onSubmit(result.value);
|
|
2829
|
+
},
|
|
2830
|
+
[value, onSubmit, setSelectedIndex]
|
|
2831
|
+
);
|
|
2832
|
+
const handleSubmit = useCallback4(
|
|
2833
|
+
(text) => {
|
|
2834
|
+
if (!shouldSubmitInput(text)) return;
|
|
2835
|
+
if (showPopup && filteredCommands[selectedIndex]) {
|
|
2836
|
+
enterSelectCommand(filteredCommands[selectedIndex]);
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
const expanded = expandPasteLabels(text.trim(), pasteStore.current);
|
|
2840
|
+
setValue("");
|
|
2841
|
+
pasteStore.current.clear();
|
|
2842
|
+
pasteIdRef.current = 0;
|
|
2843
|
+
onSubmit(expanded);
|
|
2844
|
+
},
|
|
2845
|
+
[showPopup, filteredCommands, selectedIndex, onSubmit, enterSelectCommand]
|
|
2846
|
+
);
|
|
2847
|
+
useInput2(
|
|
2848
|
+
(_input, key) => {
|
|
2849
|
+
if (!showPopup) return;
|
|
2850
|
+
const action = getAutocompletePopupAction(key);
|
|
2851
|
+
if (action === "previous" || action === "next") {
|
|
2852
|
+
setSelectedIndex(
|
|
2853
|
+
(prev) => moveAutocompleteSelection(prev, filteredCommands.length, action)
|
|
2854
|
+
);
|
|
2855
|
+
} else if (action === "close") {
|
|
2856
|
+
setShowPopup(false);
|
|
2857
|
+
} else if (action === "complete") {
|
|
2858
|
+
const cmd = filteredCommands[selectedIndex];
|
|
2859
|
+
if (cmd) tabCompleteCommand(cmd);
|
|
2860
|
+
}
|
|
2861
|
+
},
|
|
2862
|
+
{ isActive: showPopup && !isDisabled }
|
|
2863
|
+
);
|
|
2864
|
+
useInput2(
|
|
2865
|
+
(_input, key) => {
|
|
2866
|
+
if (getPendingPromptInputAction(key) === "cancelQueue" && pendingPrompt) {
|
|
2867
|
+
onCancelQueue?.();
|
|
2868
|
+
}
|
|
2869
|
+
},
|
|
2870
|
+
{ isActive: !!pendingPrompt }
|
|
2871
|
+
);
|
|
2872
|
+
const borderColor = isAborting ? "yellow" : pendingPrompt ? "cyan" : isDisabled ? "gray" : "green";
|
|
2873
|
+
const innerWidth = Math.max(1, terminalColumns - BORDER_HORIZONTAL);
|
|
2874
|
+
const topBorder = (() => {
|
|
2875
|
+
if (sessionName) {
|
|
2876
|
+
const label = ` "${sessionName}" `;
|
|
2877
|
+
const rightPad = 2;
|
|
2878
|
+
const leftLen = Math.max(0, innerWidth - label.length - rightPad);
|
|
2879
|
+
return { left: "\u250C" + "\u2500".repeat(leftLen), label, right: "\u2500".repeat(rightPad) + "\u2510" };
|
|
2880
|
+
}
|
|
2881
|
+
return { left: "\u250C" + "\u2500".repeat(innerWidth), label: "", right: "\u2510" };
|
|
2882
|
+
})();
|
|
2883
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
|
|
2884
|
+
showPopup && /* @__PURE__ */ jsx6(
|
|
2885
|
+
SlashAutocomplete,
|
|
2886
|
+
{
|
|
2887
|
+
commands: filteredCommands,
|
|
2888
|
+
selectedIndex,
|
|
2889
|
+
visible: showPopup,
|
|
2890
|
+
isSubcommandMode
|
|
2891
|
+
}
|
|
2892
|
+
),
|
|
2893
|
+
/* @__PURE__ */ jsxs5(Text7, { color: borderColor, children: [
|
|
2894
|
+
topBorder.left,
|
|
2895
|
+
topBorder.label ? /* @__PURE__ */ jsx6(Text7, { backgroundColor: borderColor, color: "black", bold: true, children: topBorder.label }) : null,
|
|
2896
|
+
topBorder.right
|
|
2897
|
+
] }),
|
|
2898
|
+
/* @__PURE__ */ jsx6(Box5, { borderStyle: "single", borderTop: false, borderColor, paddingLeft: 1, children: isAborting ? /* @__PURE__ */ jsx6(Text7, { color: "yellow", children: " Interrupting..." }) : pendingPrompt ? /* @__PURE__ */ jsxs5(Text7, { color: "cyan", children: [
|
|
2899
|
+
" ",
|
|
2900
|
+
"Queued: ",
|
|
2901
|
+
pendingPrompt.length > 50 ? pendingPrompt.slice(0, 47) + "..." : pendingPrompt,
|
|
2902
|
+
" ",
|
|
2903
|
+
/* @__PURE__ */ jsx6(Text7, { dimColor: true, children: "(Backspace to cancel)" })
|
|
2904
|
+
] }) : isDisabled ? /* @__PURE__ */ jsx6(WaveText, { text: " Waiting for response... (ESC to interrupt)" }) : /* @__PURE__ */ jsxs5(Box5, { children: [
|
|
2905
|
+
/* @__PURE__ */ jsx6(Text7, { color: "green", bold: true, children: "> " }),
|
|
2906
|
+
/* @__PURE__ */ jsx6(
|
|
2907
|
+
CjkTextInput,
|
|
2908
|
+
{
|
|
2909
|
+
value,
|
|
2910
|
+
onChange: (v) => {
|
|
2911
|
+
setValue(v);
|
|
2912
|
+
setCursorHint(null);
|
|
2913
|
+
},
|
|
2914
|
+
onSubmit: handleSubmit,
|
|
2915
|
+
onPaste: handlePaste,
|
|
2916
|
+
placeholder: "Type a message or /help",
|
|
2917
|
+
availableWidth,
|
|
2918
|
+
cursorHint
|
|
2919
|
+
}
|
|
2920
|
+
)
|
|
2921
|
+
] }) })
|
|
2922
|
+
] });
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
// src/ui/ConfirmPrompt.tsx
|
|
2926
|
+
import { useState as useState7, useRef as useRef5, useCallback as useCallback5 } from "react";
|
|
2927
|
+
import { Box as Box6, Text as Text8, useInput as useInput3 } from "ink";
|
|
2928
|
+
|
|
2929
|
+
// src/ui/flows/selection-flow.ts
|
|
2930
|
+
function createSelectionFlowState() {
|
|
2931
|
+
return { selectedIndex: 0, scrollOffset: 0, resolved: false };
|
|
2932
|
+
}
|
|
2933
|
+
function getVerticalSelectionInputAction(key) {
|
|
2934
|
+
if (key.escape === true) return "cancel";
|
|
2935
|
+
if (key.upArrow === true) return "previous";
|
|
2936
|
+
if (key.downArrow === true) return "next";
|
|
2937
|
+
if (key.return === true) return "select";
|
|
2938
|
+
return void 0;
|
|
2939
|
+
}
|
|
2940
|
+
function getDirectionalSelectionInputAction(key) {
|
|
2941
|
+
if (key.escape === true) return "cancel";
|
|
2942
|
+
if (key.leftArrow === true || key.upArrow === true) return "previous";
|
|
2943
|
+
if (key.rightArrow === true || key.downArrow === true) return "next";
|
|
2944
|
+
if (key.return === true) return "select";
|
|
2945
|
+
return void 0;
|
|
2946
|
+
}
|
|
2947
|
+
function applySelectionInput(state, action, options) {
|
|
2948
|
+
if (state.resolved) {
|
|
2949
|
+
return { state, effect: { type: "none" } };
|
|
2950
|
+
}
|
|
2951
|
+
if (action === "cancel") {
|
|
2952
|
+
return { state: { ...state, resolved: true }, effect: { type: "cancel" } };
|
|
2953
|
+
}
|
|
2954
|
+
if (options.enabled === false || options.itemCount === 0) {
|
|
2955
|
+
return { state, effect: { type: "none" } };
|
|
2956
|
+
}
|
|
2957
|
+
if (action === "select") {
|
|
2958
|
+
const index = clampIndex(state.selectedIndex, options.itemCount);
|
|
2959
|
+
return {
|
|
2960
|
+
state: { ...state, selectedIndex: index, resolved: true },
|
|
2961
|
+
effect: { type: "select", index }
|
|
2962
|
+
};
|
|
2963
|
+
}
|
|
2964
|
+
const selectedIndex = moveSelection(state.selectedIndex, action, options);
|
|
2965
|
+
const scrollOffset = resolveScrollOffset(selectedIndex, state.scrollOffset, options);
|
|
2966
|
+
return { state: { ...state, selectedIndex, scrollOffset }, effect: { type: "none" } };
|
|
2967
|
+
}
|
|
2968
|
+
function normalizeSelectionState(state, options) {
|
|
2969
|
+
if (options.itemCount === 0) {
|
|
2970
|
+
return { ...state, selectedIndex: 0, scrollOffset: 0 };
|
|
2971
|
+
}
|
|
2972
|
+
const selectedIndex = clampIndex(state.selectedIndex, options.itemCount);
|
|
2973
|
+
const scrollOffset = resolveScrollOffset(selectedIndex, state.scrollOffset, options);
|
|
2974
|
+
if (selectedIndex === state.selectedIndex && scrollOffset === state.scrollOffset) {
|
|
2975
|
+
return state;
|
|
2976
|
+
}
|
|
2977
|
+
return {
|
|
2978
|
+
...state,
|
|
2979
|
+
selectedIndex,
|
|
2980
|
+
scrollOffset
|
|
2981
|
+
};
|
|
2982
|
+
}
|
|
2983
|
+
function moveSelection(selectedIndex, action, options) {
|
|
2984
|
+
if (action === "previous") {
|
|
2985
|
+
if (options.wrap === true && selectedIndex === 0) return options.itemCount - 1;
|
|
2986
|
+
return Math.max(0, selectedIndex - 1);
|
|
2987
|
+
}
|
|
2988
|
+
if (options.wrap === true && selectedIndex === options.itemCount - 1) return 0;
|
|
2989
|
+
return Math.min(options.itemCount - 1, selectedIndex + 1);
|
|
2990
|
+
}
|
|
2991
|
+
function resolveScrollOffset(selectedIndex, scrollOffset, options) {
|
|
2992
|
+
const maxVisible = options.maxVisible ?? options.itemCount;
|
|
2993
|
+
if (maxVisible <= 0) return 0;
|
|
2994
|
+
if (selectedIndex < scrollOffset) return selectedIndex;
|
|
2995
|
+
if (selectedIndex >= scrollOffset + maxVisible) return selectedIndex - maxVisible + 1;
|
|
2996
|
+
return Math.max(0, scrollOffset);
|
|
2997
|
+
}
|
|
2998
|
+
function clampIndex(index, itemCount) {
|
|
2999
|
+
return Math.min(Math.max(index, 0), itemCount - 1);
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
// src/ui/flows/confirm-prompt-flow.ts
|
|
3003
|
+
function getConfirmPromptInputAction(input, key, optionCount) {
|
|
3004
|
+
const action = getDirectionalSelectionInputAction({ ...key, escape: false });
|
|
3005
|
+
if (action !== void 0) {
|
|
3006
|
+
return action;
|
|
3007
|
+
}
|
|
3008
|
+
if (optionCount === 2 && input === "y") {
|
|
3009
|
+
return { type: "shortcut", index: 0 };
|
|
3010
|
+
}
|
|
3011
|
+
if (optionCount === 2 && input === "n") {
|
|
3012
|
+
return { type: "shortcut", index: 1 };
|
|
3013
|
+
}
|
|
3014
|
+
return void 0;
|
|
3015
|
+
}
|
|
3016
|
+
function applyConfirmPromptInput(state, action, optionCount) {
|
|
3017
|
+
if (state.resolved) {
|
|
3018
|
+
return { state, effect: { type: "none" } };
|
|
3019
|
+
}
|
|
3020
|
+
if (typeof action !== "string") {
|
|
3021
|
+
return {
|
|
3022
|
+
state: { ...state, selectedIndex: action.index, resolved: true },
|
|
3023
|
+
effect: { type: "select", index: action.index }
|
|
3024
|
+
};
|
|
3025
|
+
}
|
|
3026
|
+
return applySelectionInput(state, action, { itemCount: optionCount });
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
// src/ui/ConfirmPrompt.tsx
|
|
3030
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
3031
|
+
function ConfirmPrompt({
|
|
3032
|
+
message,
|
|
3033
|
+
options = ["Yes", "No"],
|
|
3034
|
+
onSelect
|
|
3035
|
+
}) {
|
|
3036
|
+
const [state, setState] = useState7(() => createSelectionFlowState());
|
|
3037
|
+
const stateRef = useRef5(state);
|
|
3038
|
+
const applyAction = useCallback5(
|
|
3039
|
+
(action) => {
|
|
3040
|
+
const result = applyConfirmPromptInput(stateRef.current, action, options.length);
|
|
3041
|
+
stateRef.current = result.state;
|
|
3042
|
+
setState(result.state);
|
|
3043
|
+
if (result.effect.type === "select") {
|
|
3044
|
+
onSelect(result.effect.index);
|
|
3045
|
+
}
|
|
3046
|
+
},
|
|
3047
|
+
[onSelect, options.length]
|
|
3048
|
+
);
|
|
3049
|
+
useInput3((input, key) => {
|
|
3050
|
+
const action = getConfirmPromptInputAction(input, key, options.length);
|
|
3051
|
+
if (action !== void 0) {
|
|
3052
|
+
applyAction(action);
|
|
3053
|
+
}
|
|
3054
|
+
});
|
|
3055
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
3056
|
+
/* @__PURE__ */ jsx7(Text8, { color: "yellow", children: message }),
|
|
3057
|
+
/* @__PURE__ */ jsx7(Box6, { marginTop: 1, children: options.map((opt, i) => /* @__PURE__ */ jsx7(Box6, { marginRight: 2, children: /* @__PURE__ */ jsxs6(
|
|
3058
|
+
Text8,
|
|
3059
|
+
{
|
|
3060
|
+
color: i === state.selectedIndex ? "cyan" : void 0,
|
|
3061
|
+
bold: i === state.selectedIndex,
|
|
3062
|
+
children: [
|
|
3063
|
+
i === state.selectedIndex ? "> " : " ",
|
|
3064
|
+
opt
|
|
3065
|
+
]
|
|
3066
|
+
}
|
|
3067
|
+
) }, opt)) }),
|
|
3068
|
+
/* @__PURE__ */ jsx7(Text8, { dimColor: true, children: " arrow keys to select, Enter to confirm" })
|
|
3069
|
+
] });
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
// src/ui/ProviderSetupPrompt.tsx
|
|
3073
|
+
import { useState as useState9 } from "react";
|
|
3074
|
+
|
|
3075
|
+
// src/ui/TextPrompt.tsx
|
|
3076
|
+
import { useState as useState8, useRef as useRef6, useCallback as useCallback6 } from "react";
|
|
3077
|
+
import { Box as Box7, Text as Text9, useInput as useInput4 } from "ink";
|
|
3078
|
+
|
|
3079
|
+
// src/ui/flows/text-prompt-flow.ts
|
|
3080
|
+
function createTextPromptFlowState() {
|
|
3081
|
+
return { value: "", resolved: false };
|
|
3082
|
+
}
|
|
3083
|
+
function getTextPromptInputAction(input, key) {
|
|
3084
|
+
if (key.escape === true) {
|
|
3085
|
+
return { type: "cancel" };
|
|
3086
|
+
}
|
|
3087
|
+
if (key.return === true) {
|
|
3088
|
+
return { type: "submit" };
|
|
3089
|
+
}
|
|
3090
|
+
if (key.backspace === true || key.delete === true) {
|
|
3091
|
+
return { type: "delete" };
|
|
3092
|
+
}
|
|
3093
|
+
if (input && key.ctrl !== true && key.meta !== true) {
|
|
3094
|
+
return { type: "insert", value: input };
|
|
3095
|
+
}
|
|
3096
|
+
return void 0;
|
|
3097
|
+
}
|
|
3098
|
+
function applyTextPromptInput(state, action, options) {
|
|
3099
|
+
if (state.resolved) {
|
|
3100
|
+
return { state, effect: { type: "none" } };
|
|
3101
|
+
}
|
|
3102
|
+
if (action.type === "cancel") {
|
|
3103
|
+
return { state: { ...state, resolved: true }, effect: { type: "cancel" } };
|
|
3104
|
+
}
|
|
3105
|
+
if (action.type === "delete") {
|
|
3106
|
+
return {
|
|
3107
|
+
state: { ...state, value: state.value.slice(0, -1), error: void 0 },
|
|
3108
|
+
effect: { type: "none" }
|
|
3109
|
+
};
|
|
3110
|
+
}
|
|
3111
|
+
if (action.type === "insert") {
|
|
3112
|
+
return {
|
|
3113
|
+
state: { ...state, value: state.value + action.value, error: void 0 },
|
|
3114
|
+
effect: { type: "none" }
|
|
3115
|
+
};
|
|
3116
|
+
}
|
|
3117
|
+
return submitTextPromptValue(state, options);
|
|
3118
|
+
}
|
|
3119
|
+
function submitTextPromptValue(state, options) {
|
|
3120
|
+
const trimmed = state.value.trim();
|
|
3121
|
+
if (!trimmed && !options.allowEmpty) {
|
|
3122
|
+
const emptyError = options.validate?.(trimmed);
|
|
3123
|
+
return {
|
|
3124
|
+
state: emptyError ? { ...state, error: emptyError } : state,
|
|
3125
|
+
effect: { type: "none" }
|
|
3126
|
+
};
|
|
3127
|
+
}
|
|
3128
|
+
const error = options.validate?.(trimmed);
|
|
3129
|
+
if (error !== void 0) {
|
|
3130
|
+
return { state: { ...state, error }, effect: { type: "none" } };
|
|
3131
|
+
}
|
|
3132
|
+
return { state: { ...state, resolved: true }, effect: { type: "submit", value: trimmed } };
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
// src/ui/TextPrompt.tsx
|
|
3136
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
3137
|
+
function TextPrompt({
|
|
3138
|
+
title,
|
|
3139
|
+
placeholder,
|
|
3140
|
+
onSubmit,
|
|
3141
|
+
onCancel,
|
|
3142
|
+
validate,
|
|
3143
|
+
allowEmpty = false,
|
|
3144
|
+
masked = false
|
|
3145
|
+
}) {
|
|
3146
|
+
const [state, setState] = useState8(() => createTextPromptFlowState());
|
|
3147
|
+
const stateRef = useRef6(state);
|
|
3148
|
+
const applyAction = useCallback6(
|
|
3149
|
+
(action) => {
|
|
3150
|
+
const result = applyTextPromptInput(stateRef.current, action, { allowEmpty, validate });
|
|
3151
|
+
stateRef.current = result.state;
|
|
3152
|
+
setState(result.state);
|
|
3153
|
+
if (result.effect.type === "cancel") {
|
|
3154
|
+
onCancel();
|
|
3155
|
+
} else if (result.effect.type === "submit") {
|
|
3156
|
+
onSubmit(result.effect.value);
|
|
3157
|
+
}
|
|
3158
|
+
},
|
|
3159
|
+
[allowEmpty, validate, onCancel, onSubmit]
|
|
3160
|
+
);
|
|
3161
|
+
useInput4((input, key) => {
|
|
3162
|
+
const action = getTextPromptInputAction(input, key);
|
|
3163
|
+
if (action !== void 0) {
|
|
3164
|
+
applyAction(action);
|
|
3165
|
+
}
|
|
3166
|
+
});
|
|
3167
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
3168
|
+
/* @__PURE__ */ jsx8(Text9, { color: "yellow", bold: true, children: title }),
|
|
3169
|
+
/* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
|
|
3170
|
+
/* @__PURE__ */ jsx8(Text9, { color: "cyan", children: "> " }),
|
|
3171
|
+
state.value ? /* @__PURE__ */ jsx8(Text9, { children: masked ? "*".repeat(state.value.length) : state.value }) : placeholder ? /* @__PURE__ */ jsx8(Text9, { dimColor: true, children: placeholder }) : null,
|
|
3172
|
+
/* @__PURE__ */ jsx8(Text9, { color: "cyan", children: "\u2588" })
|
|
3173
|
+
] }),
|
|
3174
|
+
state.error && /* @__PURE__ */ jsx8(Text9, { color: "red", children: state.error }),
|
|
3175
|
+
/* @__PURE__ */ jsx8(Text9, { dimColor: true, children: " Enter Submit Esc Cancel" })
|
|
3176
|
+
] });
|
|
3177
|
+
}
|
|
3178
|
+
|
|
3179
|
+
// src/ui/ProviderSetupPrompt.tsx
|
|
3180
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
3181
|
+
function ProviderSetupPrompt({
|
|
3182
|
+
type,
|
|
3183
|
+
providerDefinitions,
|
|
3184
|
+
onSubmit,
|
|
3185
|
+
onCancel
|
|
3186
|
+
}) {
|
|
3187
|
+
const [state, setState] = useState9(
|
|
3188
|
+
() => createProviderSetupFlow(type, providerDefinitions)
|
|
3189
|
+
);
|
|
3190
|
+
const step = getProviderSetupStep(state);
|
|
3191
|
+
const handleStepSubmit = (rawValue) => {
|
|
3192
|
+
const result = submitProviderSetupValue(state, rawValue);
|
|
3193
|
+
if (result.status === "next") {
|
|
3194
|
+
setState(result.state);
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
3197
|
+
if (result.status === "complete") {
|
|
3198
|
+
onSubmit(result.input);
|
|
3199
|
+
}
|
|
3200
|
+
};
|
|
3201
|
+
return /* @__PURE__ */ jsx9(
|
|
3202
|
+
TextPrompt,
|
|
3203
|
+
{
|
|
3204
|
+
title: step.title,
|
|
3205
|
+
placeholder: step.defaultValue,
|
|
3206
|
+
allowEmpty: step.defaultValue !== void 0,
|
|
3207
|
+
masked: step.masked,
|
|
3208
|
+
validate: (value) => validateProviderSetupValue(step, value),
|
|
3209
|
+
onSubmit: handleStepSubmit,
|
|
3210
|
+
onCancel
|
|
3211
|
+
},
|
|
3212
|
+
`${type}-${step.key}`
|
|
3213
|
+
);
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
// src/ui/PermissionPrompt.tsx
|
|
3217
|
+
import React9 from "react";
|
|
3218
|
+
import { Box as Box8, Text as Text10, useInput as useInput5 } from "ink";
|
|
3219
|
+
|
|
3220
|
+
// src/ui/flows/permission-prompt-flow.ts
|
|
3221
|
+
var PERMISSION_PROMPT_OPTIONS = ["Allow", "Allow always (this session)", "Deny"];
|
|
3222
|
+
function getPermissionPromptInputAction(input, key) {
|
|
3223
|
+
const action = getDirectionalSelectionInputAction({ ...key, escape: false });
|
|
3224
|
+
if (action !== void 0) {
|
|
3225
|
+
return action;
|
|
3226
|
+
}
|
|
3227
|
+
if (input === "y" || input === "1") {
|
|
3228
|
+
return { type: "shortcut", index: 0 };
|
|
3229
|
+
}
|
|
3230
|
+
if (input === "a" || input === "2") {
|
|
3231
|
+
return { type: "shortcut", index: 1 };
|
|
3232
|
+
}
|
|
3233
|
+
if (input === "n" || input === "d" || input === "3") {
|
|
3234
|
+
return { type: "shortcut", index: 2 };
|
|
3235
|
+
}
|
|
3236
|
+
return void 0;
|
|
3237
|
+
}
|
|
3238
|
+
function applyPermissionPromptInput(state, action) {
|
|
3239
|
+
if (state.resolved) {
|
|
3240
|
+
return { state, effect: { type: "none" } };
|
|
3241
|
+
}
|
|
3242
|
+
if (typeof action !== "string") {
|
|
3243
|
+
return resolvePermissionIndex(state, action.index);
|
|
3244
|
+
}
|
|
3245
|
+
const result = applySelectionInput(state, action, {
|
|
3246
|
+
itemCount: PERMISSION_PROMPT_OPTIONS.length
|
|
3247
|
+
});
|
|
3248
|
+
if (result.effect.type !== "select") {
|
|
3249
|
+
return { state: result.state, effect: { type: "none" } };
|
|
3250
|
+
}
|
|
3251
|
+
return {
|
|
3252
|
+
state: result.state,
|
|
3253
|
+
effect: { type: "resolve", decision: getPermissionDecision(result.effect.index) }
|
|
3254
|
+
};
|
|
3255
|
+
}
|
|
3256
|
+
function getPermissionDecision(index) {
|
|
3257
|
+
if (index === 0) return true;
|
|
3258
|
+
if (index === 1) return "allow-session";
|
|
3259
|
+
return false;
|
|
3260
|
+
}
|
|
3261
|
+
function resolvePermissionIndex(state, index) {
|
|
3262
|
+
return {
|
|
3263
|
+
state: { ...state, selectedIndex: index, resolved: true },
|
|
3264
|
+
effect: { type: "resolve", decision: getPermissionDecision(index) }
|
|
3265
|
+
};
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
// src/ui/PermissionPrompt.tsx
|
|
3269
|
+
import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
3270
|
+
function formatArgs(args) {
|
|
3271
|
+
const entries = Object.entries(args);
|
|
3272
|
+
if (entries.length === 0) return "(no arguments)";
|
|
3273
|
+
return entries.map(([k, v]) => `${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`).join(", ");
|
|
3274
|
+
}
|
|
3275
|
+
function PermissionPrompt({ request }) {
|
|
3276
|
+
const [state, setState] = React9.useState(() => createSelectionFlowState());
|
|
3277
|
+
const stateRef = React9.useRef(state);
|
|
3278
|
+
const prevRequestRef = React9.useRef(request);
|
|
3279
|
+
if (prevRequestRef.current !== request) {
|
|
3280
|
+
prevRequestRef.current = request;
|
|
3281
|
+
const nextState = createSelectionFlowState();
|
|
3282
|
+
stateRef.current = nextState;
|
|
3283
|
+
setState(nextState);
|
|
3284
|
+
}
|
|
3285
|
+
const applyAction = React9.useCallback(
|
|
3286
|
+
(action) => {
|
|
3287
|
+
const result = applyPermissionPromptInput(stateRef.current, action);
|
|
3288
|
+
stateRef.current = result.state;
|
|
3289
|
+
setState(result.state);
|
|
3290
|
+
if (result.effect.type === "resolve") {
|
|
3291
|
+
request.resolve(result.effect.decision);
|
|
3292
|
+
}
|
|
3293
|
+
},
|
|
3294
|
+
[request]
|
|
3295
|
+
);
|
|
3296
|
+
useInput5((input, key) => {
|
|
3297
|
+
const action = getPermissionPromptInputAction(input, key);
|
|
3298
|
+
if (action !== void 0) {
|
|
3299
|
+
applyAction(action);
|
|
3300
|
+
}
|
|
3301
|
+
});
|
|
3302
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
3303
|
+
/* @__PURE__ */ jsx10(Text10, { color: "yellow", bold: true, children: "[Permission Required]" }),
|
|
3304
|
+
/* @__PURE__ */ jsxs8(Text10, { children: [
|
|
3305
|
+
"Tool:",
|
|
3306
|
+
" ",
|
|
3307
|
+
/* @__PURE__ */ jsx10(Text10, { color: "cyan", bold: true, children: request.toolName })
|
|
3308
|
+
] }),
|
|
3309
|
+
/* @__PURE__ */ jsxs8(Text10, { dimColor: true, children: [
|
|
3310
|
+
" ",
|
|
3311
|
+
formatArgs(request.toolArgs)
|
|
3312
|
+
] }),
|
|
3313
|
+
/* @__PURE__ */ jsx10(Box8, { marginTop: 1, children: PERMISSION_PROMPT_OPTIONS.map((opt, i) => /* @__PURE__ */ jsx10(Box8, { marginRight: 2, children: /* @__PURE__ */ jsxs8(
|
|
3314
|
+
Text10,
|
|
3315
|
+
{
|
|
3316
|
+
color: i === state.selectedIndex ? "cyan" : void 0,
|
|
3317
|
+
bold: i === state.selectedIndex,
|
|
3318
|
+
children: [
|
|
3319
|
+
i === state.selectedIndex ? "> " : " ",
|
|
3320
|
+
opt
|
|
3321
|
+
]
|
|
3322
|
+
}
|
|
3323
|
+
) }, opt)) }),
|
|
3324
|
+
/* @__PURE__ */ jsx10(Text10, { dimColor: true, children: " left/right to select, Enter to confirm" })
|
|
3325
|
+
] });
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
// src/ui/StreamingIndicator.tsx
|
|
3329
|
+
import { Box as Box9, Text as Text11 } from "ink";
|
|
3330
|
+
import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
3331
|
+
function getToolStyle(t) {
|
|
3332
|
+
if (t.isRunning) return { color: "yellow", icon: "\u27F3", strikethrough: false };
|
|
3333
|
+
if (t.result === "error") return { color: "red", icon: "\u2717", strikethrough: true };
|
|
3334
|
+
if (t.result === "denied") return { color: "yellowBright", icon: "\u2298", strikethrough: true };
|
|
3335
|
+
return { color: "green", icon: "\u2713", strikethrough: false };
|
|
3336
|
+
}
|
|
3337
|
+
function StreamingIndicator({ text, activeTools }) {
|
|
3338
|
+
const hasTools = activeTools.length > 0;
|
|
3339
|
+
const hasText = text.length > 0;
|
|
3340
|
+
if (!hasTools && !hasText) {
|
|
3341
|
+
return /* @__PURE__ */ jsx11(Fragment3, {});
|
|
3342
|
+
}
|
|
3343
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
|
|
3344
|
+
hasTools && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 1, children: [
|
|
3345
|
+
/* @__PURE__ */ jsx11(Text11, { color: "white", bold: true, children: "Tools:" }),
|
|
3346
|
+
/* @__PURE__ */ jsx11(Text11, { children: " " }),
|
|
3347
|
+
activeTools.map((t, i) => {
|
|
3348
|
+
const { color, icon, strikethrough } = getToolStyle(t);
|
|
3349
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
|
|
3350
|
+
/* @__PURE__ */ jsxs9(Text11, { color, strikethrough, children: [
|
|
3351
|
+
" ",
|
|
3352
|
+
icon,
|
|
3353
|
+
" ",
|
|
3354
|
+
t.toolName,
|
|
3355
|
+
"(",
|
|
3356
|
+
t.firstArg,
|
|
3357
|
+
")"
|
|
3358
|
+
] }),
|
|
3359
|
+
t.diffLines && t.diffLines.length > 0 && /* @__PURE__ */ jsx11(DiffBlock, { file: t.diffFile, lines: t.diffLines })
|
|
3360
|
+
] }, `${t.toolName}-${i}`);
|
|
3361
|
+
})
|
|
3362
|
+
] }),
|
|
3363
|
+
hasText && /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginBottom: 1, children: [
|
|
3364
|
+
/* @__PURE__ */ jsx11(Text11, { color: "cyan", bold: true, children: "Robota:" }),
|
|
3365
|
+
/* @__PURE__ */ jsx11(Text11, { children: " " }),
|
|
3366
|
+
/* @__PURE__ */ jsx11(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsx11(Text11, { wrap: "wrap", children: renderMarkdown(text) }) })
|
|
3367
|
+
] })
|
|
3368
|
+
] });
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3371
|
+
// src/ui/PluginTUI.tsx
|
|
3372
|
+
import { useState as useState12, useCallback as useCallback8 } from "react";
|
|
3373
|
+
|
|
3374
|
+
// src/ui/MenuSelect.tsx
|
|
3375
|
+
import { useState as useState10, useCallback as useCallback7, useRef as useRef7 } from "react";
|
|
3376
|
+
import { Box as Box10, Text as Text12, useInput as useInput6 } from "ink";
|
|
3377
|
+
import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
3378
|
+
function MenuSelect({
|
|
3379
|
+
title,
|
|
3380
|
+
items,
|
|
3381
|
+
onSelect,
|
|
3382
|
+
onBack,
|
|
3383
|
+
loading,
|
|
3384
|
+
error
|
|
3385
|
+
}) {
|
|
3386
|
+
const [state, setState] = useState10(() => createSelectionFlowState());
|
|
3387
|
+
const stateRef = useRef7(state);
|
|
3388
|
+
const isEnabled = !loading && !error;
|
|
3389
|
+
const applyAction = useCallback7(
|
|
3390
|
+
(action) => {
|
|
3391
|
+
const result = applySelectionInput(stateRef.current, action, {
|
|
3392
|
+
itemCount: items.length,
|
|
3393
|
+
enabled: isEnabled
|
|
3394
|
+
});
|
|
3395
|
+
stateRef.current = result.state;
|
|
3396
|
+
setState(result.state);
|
|
3397
|
+
if (result.effect.type === "cancel") {
|
|
3398
|
+
onBack();
|
|
3399
|
+
} else if (result.effect.type === "select") {
|
|
3400
|
+
const item = items[result.effect.index];
|
|
3401
|
+
if (item !== void 0) {
|
|
3402
|
+
onSelect(item.value);
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
},
|
|
3406
|
+
[isEnabled, items, onBack, onSelect]
|
|
3407
|
+
);
|
|
3408
|
+
useInput6((input, key) => {
|
|
3409
|
+
const action = getVerticalSelectionInputAction(key);
|
|
3410
|
+
if (action !== void 0) {
|
|
3411
|
+
applyAction(action);
|
|
3412
|
+
}
|
|
3413
|
+
});
|
|
3414
|
+
const normalizedState = normalizeSelectionState(state, { itemCount: items.length });
|
|
3415
|
+
if (normalizedState !== state) {
|
|
3416
|
+
stateRef.current = normalizedState;
|
|
3417
|
+
}
|
|
3418
|
+
const selected = normalizedState.selectedIndex;
|
|
3419
|
+
return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
3420
|
+
/* @__PURE__ */ jsx12(Text12, { color: "yellow", bold: true, children: title }),
|
|
3421
|
+
loading && /* @__PURE__ */ jsx12(Box10, { marginTop: 1, children: /* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Loading..." }) }),
|
|
3422
|
+
error && /* @__PURE__ */ jsxs10(Box10, { marginTop: 1, flexDirection: "column", children: [
|
|
3423
|
+
/* @__PURE__ */ jsx12(Text12, { color: "red", children: error }),
|
|
3424
|
+
/* @__PURE__ */ jsx12(Text12, { dimColor: true, children: "Press Esc to go back" })
|
|
3425
|
+
] }),
|
|
3426
|
+
!loading && !error && /* @__PURE__ */ jsx12(Box10, { flexDirection: "column", marginTop: 1, children: items.map((item, i) => /* @__PURE__ */ jsxs10(Box10, { children: [
|
|
3427
|
+
/* @__PURE__ */ jsxs10(Text12, { color: i === selected ? "cyan" : void 0, bold: i === selected, children: [
|
|
3428
|
+
i === selected ? "> " : " ",
|
|
3429
|
+
item.label
|
|
3430
|
+
] }),
|
|
3431
|
+
item.hint && /* @__PURE__ */ jsxs10(Text12, { dimColor: true, children: [
|
|
3432
|
+
" ",
|
|
3433
|
+
item.hint
|
|
3434
|
+
] })
|
|
3435
|
+
] }, item.value)) }),
|
|
3436
|
+
/* @__PURE__ */ jsx12(Text12, { dimColor: true, children: loading || error ? "" : " \u2191\u2193 Navigate Enter Select Esc Back" })
|
|
3437
|
+
] });
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
// src/ui/plugin-tui-handlers.ts
|
|
3441
|
+
function handleMainSelect(value, nav) {
|
|
3442
|
+
if (value === "marketplace") {
|
|
3443
|
+
nav.push({ screen: "marketplace-list" });
|
|
3444
|
+
} else if (value === "installed") {
|
|
3445
|
+
nav.push({ screen: "installed-list" });
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
function handleMarketplaceListSelect(value, nav) {
|
|
3449
|
+
if (value === "__add__") {
|
|
3450
|
+
nav.push({ screen: "marketplace-add" });
|
|
3451
|
+
} else {
|
|
3452
|
+
nav.push({ screen: "marketplace-action", context: { marketplace: value } });
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
function handleMarketplaceActionSelect(value, marketplace, callbacks, nav) {
|
|
3456
|
+
if (value === "browse") {
|
|
3457
|
+
nav.push({ screen: "marketplace-browse", context: { marketplace } });
|
|
3458
|
+
} else if (value === "update") {
|
|
3459
|
+
callbacks.marketplaceUpdate(marketplace).then(() => {
|
|
3460
|
+
nav.notify(`Updated marketplace "${marketplace}".`);
|
|
3461
|
+
nav.pop();
|
|
3462
|
+
}).catch((err) => {
|
|
3463
|
+
nav.notify(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
3464
|
+
});
|
|
3465
|
+
} else if (value === "remove") {
|
|
3466
|
+
nav.setConfirm({
|
|
3467
|
+
message: `Remove marketplace "${marketplace}" and all its plugins?`,
|
|
3468
|
+
onConfirm: () => {
|
|
3469
|
+
nav.setConfirm(void 0);
|
|
3470
|
+
callbacks.marketplaceRemove(marketplace).then(() => {
|
|
3471
|
+
nav.notify(`Removed marketplace "${marketplace}".`);
|
|
3472
|
+
nav.popN(2);
|
|
3473
|
+
}).catch((err) => {
|
|
3474
|
+
nav.notify(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
3475
|
+
});
|
|
3476
|
+
},
|
|
3477
|
+
onCancel: () => nav.setConfirm(void 0)
|
|
3478
|
+
});
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
function handleMarketplaceBrowseSelect(value, marketplace, items, nav) {
|
|
3482
|
+
const fullId = `${value}@${marketplace}`;
|
|
3483
|
+
const item = items.find((i) => i.value === value);
|
|
3484
|
+
if (item?.hint === "installed") {
|
|
3485
|
+
nav.push({ screen: "installed-action", context: { pluginId: fullId } });
|
|
3486
|
+
} else {
|
|
3487
|
+
nav.push({ screen: "marketplace-install-scope", context: { marketplace, pluginId: fullId } });
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
function handleInstallScopeSelect(value, pluginId, callbacks, nav) {
|
|
3491
|
+
const scope = value;
|
|
3492
|
+
callbacks.install(pluginId, scope).then(() => {
|
|
3493
|
+
nav.notify(`Installed plugin "${pluginId}" (${scope} scope).`);
|
|
3494
|
+
nav.popN(2);
|
|
3495
|
+
}).catch((err) => {
|
|
3496
|
+
nav.notify(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
3497
|
+
});
|
|
3498
|
+
}
|
|
3499
|
+
function handleInstalledListSelect(value, callbacks, nav) {
|
|
3500
|
+
nav.setConfirm({
|
|
3501
|
+
message: `Uninstall plugin "${value}"?`,
|
|
3502
|
+
onConfirm: () => {
|
|
3503
|
+
nav.setConfirm(void 0);
|
|
3504
|
+
callbacks.uninstall(value).then(() => {
|
|
3505
|
+
nav.notify(`Uninstalled plugin "${value}".`);
|
|
3506
|
+
nav.refresh();
|
|
3507
|
+
}).catch((err) => {
|
|
3508
|
+
nav.notify(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
3509
|
+
});
|
|
3510
|
+
},
|
|
3511
|
+
onCancel: () => nav.setConfirm(void 0)
|
|
3512
|
+
});
|
|
3513
|
+
}
|
|
3514
|
+
function handleInstalledActionSelect(value, pluginId, callbacks, nav) {
|
|
3515
|
+
if (value === "uninstall") {
|
|
3516
|
+
nav.setConfirm({
|
|
3517
|
+
message: `Uninstall plugin "${pluginId}"?`,
|
|
3518
|
+
onConfirm: () => {
|
|
3519
|
+
nav.setConfirm(void 0);
|
|
3520
|
+
callbacks.uninstall(pluginId).then(() => {
|
|
3521
|
+
nav.notify(`Uninstalled plugin "${pluginId}".`);
|
|
3522
|
+
nav.popN(2);
|
|
3523
|
+
}).catch((err) => {
|
|
3524
|
+
nav.notify(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
3525
|
+
});
|
|
3526
|
+
},
|
|
3527
|
+
onCancel: () => nav.setConfirm(void 0)
|
|
3528
|
+
});
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3532
|
+
// src/ui/hooks/usePluginScreenData.ts
|
|
3533
|
+
import { useState as useState11, useEffect as useEffect3 } from "react";
|
|
3534
|
+
function usePluginScreenData(screen, marketplace, callbacks, refreshCounter, stackLength) {
|
|
3535
|
+
const [items, setItems] = useState11([]);
|
|
3536
|
+
const [loading, setLoading] = useState11(false);
|
|
3537
|
+
const [error, setError] = useState11();
|
|
3538
|
+
useEffect3(() => {
|
|
3539
|
+
setItems([]);
|
|
3540
|
+
setError(void 0);
|
|
3541
|
+
if (screen === "marketplace-list") {
|
|
3542
|
+
setLoading(true);
|
|
3543
|
+
callbacks.marketplaceList().then((sources) => {
|
|
3544
|
+
const baseItems = [{ label: "Add Marketplace", value: "__add__" }];
|
|
3545
|
+
const sourceItems = sources.map((s) => ({
|
|
3546
|
+
label: s.name,
|
|
3547
|
+
value: s.name,
|
|
3548
|
+
hint: s.type
|
|
3549
|
+
}));
|
|
3550
|
+
setItems([...baseItems, ...sourceItems]);
|
|
3551
|
+
setLoading(false);
|
|
3552
|
+
}).catch((err) => {
|
|
3553
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
3554
|
+
setLoading(false);
|
|
3555
|
+
});
|
|
3556
|
+
} else if (screen === "marketplace-browse") {
|
|
3557
|
+
const mp = marketplace ?? "";
|
|
3558
|
+
setLoading(true);
|
|
3559
|
+
callbacks.listAvailablePlugins(mp).then((plugins) => {
|
|
3560
|
+
setItems(
|
|
3561
|
+
plugins.map((p) => ({
|
|
3562
|
+
label: p.name,
|
|
3563
|
+
value: p.name,
|
|
3564
|
+
hint: p.installed ? "installed" : p.description
|
|
3565
|
+
}))
|
|
3566
|
+
);
|
|
3567
|
+
setLoading(false);
|
|
3568
|
+
}).catch((err) => {
|
|
3569
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
3570
|
+
setLoading(false);
|
|
3571
|
+
});
|
|
3572
|
+
} else if (screen === "installed-list") {
|
|
3573
|
+
setLoading(true);
|
|
3574
|
+
callbacks.listInstalled().then((plugins) => {
|
|
3575
|
+
setItems(
|
|
3576
|
+
plugins.map((p) => ({
|
|
3577
|
+
label: p.name,
|
|
3578
|
+
value: p.name,
|
|
3579
|
+
hint: p.description
|
|
3580
|
+
}))
|
|
3581
|
+
);
|
|
3582
|
+
setLoading(false);
|
|
3583
|
+
}).catch((err) => {
|
|
3584
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
3585
|
+
setLoading(false);
|
|
3586
|
+
});
|
|
3587
|
+
}
|
|
3588
|
+
}, [stackLength, screen, marketplace, callbacks, refreshCounter]);
|
|
3589
|
+
return { items, loading, error };
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
// src/ui/PluginTUI.tsx
|
|
3593
|
+
import { jsx as jsx13 } from "react/jsx-runtime";
|
|
3594
|
+
function PluginTUI({ callbacks, onClose, addMessage }) {
|
|
3595
|
+
const [stack, setStack] = useState12([{ screen: "main" }]);
|
|
3596
|
+
const [confirm, setConfirm] = useState12();
|
|
3597
|
+
const [refreshCounter, setRefreshCounter] = useState12(0);
|
|
3598
|
+
const current = stack[stack.length - 1] ?? { screen: "main" };
|
|
3599
|
+
const push = useCallback8((state) => {
|
|
3600
|
+
setStack((prev) => [...prev, state]);
|
|
3601
|
+
}, []);
|
|
3602
|
+
const pop = useCallback8(() => {
|
|
3603
|
+
setStack((prev) => {
|
|
3604
|
+
if (prev.length <= 1) {
|
|
3605
|
+
onClose();
|
|
3606
|
+
return prev;
|
|
3607
|
+
}
|
|
3608
|
+
return prev.slice(0, -1);
|
|
3609
|
+
});
|
|
3610
|
+
}, [onClose]);
|
|
3611
|
+
const popN = useCallback8(
|
|
3612
|
+
(n) => {
|
|
3613
|
+
setStack((prev) => {
|
|
3614
|
+
const next = prev.slice(0, Math.max(1, prev.length - n));
|
|
3615
|
+
if (next.length === 0) {
|
|
3616
|
+
onClose();
|
|
3617
|
+
return prev;
|
|
3618
|
+
}
|
|
3619
|
+
return next;
|
|
3620
|
+
});
|
|
3621
|
+
},
|
|
3622
|
+
[onClose]
|
|
3623
|
+
);
|
|
3624
|
+
const notify = useCallback8(
|
|
3625
|
+
(content) => {
|
|
3626
|
+
addMessage?.({ role: "system", content });
|
|
3627
|
+
},
|
|
3628
|
+
[addMessage]
|
|
3629
|
+
);
|
|
3630
|
+
const refresh = useCallback8(() => {
|
|
3631
|
+
setRefreshCounter((c) => c + 1);
|
|
3632
|
+
}, []);
|
|
3633
|
+
const setConfirmNav = useCallback8(
|
|
3634
|
+
(state) => setConfirm(state),
|
|
3635
|
+
[setConfirm]
|
|
3636
|
+
);
|
|
3637
|
+
const pushNav = useCallback8(
|
|
3638
|
+
(state) => push({ screen: state.screen, context: state.context }),
|
|
3639
|
+
[push]
|
|
3640
|
+
);
|
|
3641
|
+
const nav = { push: pushNav, pop, popN, notify, setConfirm: setConfirmNav, refresh };
|
|
3642
|
+
const { items, loading, error } = usePluginScreenData(
|
|
3643
|
+
current.screen,
|
|
3644
|
+
current.context?.marketplace,
|
|
3645
|
+
callbacks,
|
|
3646
|
+
refreshCounter,
|
|
3647
|
+
stack.length
|
|
3648
|
+
);
|
|
3649
|
+
const handleSelect = useCallback8(
|
|
3650
|
+
(value) => {
|
|
3651
|
+
const screen2 = current.screen;
|
|
3652
|
+
const ctx = current.context;
|
|
3653
|
+
if (screen2 === "main") handleMainSelect(value, nav);
|
|
3654
|
+
else if (screen2 === "marketplace-list") handleMarketplaceListSelect(value, nav);
|
|
3655
|
+
else if (screen2 === "marketplace-action")
|
|
3656
|
+
handleMarketplaceActionSelect(value, ctx?.marketplace ?? "", callbacks, nav);
|
|
3657
|
+
else if (screen2 === "marketplace-browse")
|
|
3658
|
+
handleMarketplaceBrowseSelect(value, ctx?.marketplace ?? "", items, nav);
|
|
3659
|
+
else if (screen2 === "marketplace-install-scope")
|
|
3660
|
+
handleInstallScopeSelect(value, ctx?.pluginId ?? "", callbacks, nav);
|
|
3661
|
+
else if (screen2 === "installed-list") handleInstalledListSelect(value, callbacks, nav);
|
|
3662
|
+
else if (screen2 === "installed-action")
|
|
3663
|
+
handleInstalledActionSelect(value, ctx?.pluginId ?? "", callbacks, nav);
|
|
3664
|
+
},
|
|
3665
|
+
[current, items, callbacks, push, pop, popN, notify, setConfirm, refresh]
|
|
3666
|
+
);
|
|
3667
|
+
const handleTextSubmit = useCallback8(
|
|
3668
|
+
(value) => {
|
|
3669
|
+
if (current.screen === "marketplace-add") {
|
|
3670
|
+
callbacks.marketplaceAdd(value).then((name) => {
|
|
3671
|
+
notify(`Added marketplace "${name}" from ${value}.`);
|
|
3672
|
+
pop();
|
|
3673
|
+
}).catch((err) => {
|
|
3674
|
+
notify(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
3675
|
+
pop();
|
|
3676
|
+
});
|
|
3677
|
+
}
|
|
3678
|
+
},
|
|
3679
|
+
[current.screen, callbacks, notify, pop]
|
|
3680
|
+
);
|
|
3681
|
+
if (confirm) {
|
|
3682
|
+
return /* @__PURE__ */ jsx13(
|
|
3683
|
+
ConfirmPrompt,
|
|
3684
|
+
{
|
|
3685
|
+
message: confirm.message,
|
|
3686
|
+
onSelect: (index) => {
|
|
3687
|
+
if (index === 0) confirm.onConfirm();
|
|
3688
|
+
else confirm.onCancel();
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
);
|
|
3692
|
+
}
|
|
3693
|
+
const screen = current.screen;
|
|
3694
|
+
if (screen === "marketplace-add") {
|
|
3695
|
+
return /* @__PURE__ */ jsx13(
|
|
3696
|
+
TextPrompt,
|
|
3697
|
+
{
|
|
3698
|
+
title: "Add Marketplace Source",
|
|
3699
|
+
placeholder: "owner/repo or git URL",
|
|
3700
|
+
onSubmit: handleTextSubmit,
|
|
3701
|
+
onCancel: pop,
|
|
3702
|
+
validate: (v) => !v.includes("/") ? "Must be owner/repo or a git URL" : void 0
|
|
3703
|
+
}
|
|
3704
|
+
);
|
|
3705
|
+
}
|
|
3706
|
+
if (screen === "marketplace-action") {
|
|
3707
|
+
return /* @__PURE__ */ jsx13(
|
|
3708
|
+
MenuSelect,
|
|
3709
|
+
{
|
|
3710
|
+
title: `Marketplace: ${current.context?.marketplace ?? ""}`,
|
|
3711
|
+
items: [
|
|
3712
|
+
{ label: "Browse plugins", value: "browse" },
|
|
3713
|
+
{ label: "Update", value: "update" },
|
|
3714
|
+
{ label: "Remove", value: "remove" }
|
|
3715
|
+
],
|
|
3716
|
+
onSelect: handleSelect,
|
|
3717
|
+
onBack: pop
|
|
3718
|
+
},
|
|
3719
|
+
stack.length
|
|
3720
|
+
);
|
|
3721
|
+
}
|
|
3722
|
+
if (screen === "marketplace-install-scope") {
|
|
3723
|
+
return /* @__PURE__ */ jsx13(
|
|
3724
|
+
MenuSelect,
|
|
3725
|
+
{
|
|
3726
|
+
title: `Install scope for "${current.context?.pluginId ?? ""}"`,
|
|
3727
|
+
items: [
|
|
3728
|
+
{ label: "User scope", value: "user" },
|
|
3729
|
+
{ label: "Project scope", value: "project" }
|
|
3730
|
+
],
|
|
3731
|
+
onSelect: handleSelect,
|
|
3732
|
+
onBack: pop
|
|
3733
|
+
},
|
|
3734
|
+
stack.length
|
|
3735
|
+
);
|
|
3736
|
+
}
|
|
3737
|
+
if (screen === "installed-action") {
|
|
3738
|
+
return /* @__PURE__ */ jsx13(
|
|
3739
|
+
MenuSelect,
|
|
3740
|
+
{
|
|
3741
|
+
title: `Plugin: ${current.context?.pluginId ?? ""}`,
|
|
3742
|
+
items: [{ label: "Uninstall", value: "uninstall" }],
|
|
3743
|
+
onSelect: handleSelect,
|
|
3744
|
+
onBack: pop
|
|
3745
|
+
},
|
|
3746
|
+
stack.length
|
|
3747
|
+
);
|
|
3748
|
+
}
|
|
3749
|
+
const titleMap = {
|
|
3750
|
+
main: "Plugin Management",
|
|
3751
|
+
"marketplace-list": "Marketplace",
|
|
3752
|
+
"marketplace-browse": `Browse: ${current.context?.marketplace ?? ""}`,
|
|
3753
|
+
"installed-list": "Installed Plugins"
|
|
3754
|
+
};
|
|
3755
|
+
const staticItemsMap = {
|
|
3756
|
+
main: [
|
|
3757
|
+
{ label: "Marketplace", value: "marketplace" },
|
|
3758
|
+
{ label: "Installed Plugins", value: "installed" }
|
|
3759
|
+
]
|
|
3760
|
+
};
|
|
3761
|
+
return /* @__PURE__ */ jsx13(
|
|
3762
|
+
MenuSelect,
|
|
3763
|
+
{
|
|
3764
|
+
title: titleMap[screen] ?? "Plugin Management",
|
|
3765
|
+
items: staticItemsMap[screen] ?? items,
|
|
3766
|
+
onSelect: handleSelect,
|
|
3767
|
+
onBack: pop,
|
|
3768
|
+
loading,
|
|
3769
|
+
error
|
|
3770
|
+
},
|
|
3771
|
+
`${screen}-${stack.length}-${refreshCounter}`
|
|
3772
|
+
);
|
|
3773
|
+
}
|
|
3774
|
+
|
|
3775
|
+
// src/ui/SessionPicker.tsx
|
|
3776
|
+
import { Box as Box12, Text as Text14 } from "ink";
|
|
3777
|
+
|
|
3778
|
+
// src/ui/ListPicker.tsx
|
|
3779
|
+
import { useState as useState13, useRef as useRef8, useCallback as useCallback9 } from "react";
|
|
3780
|
+
import { Box as Box11, Text as Text13, useInput as useInput7 } from "ink";
|
|
3781
|
+
import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
3782
|
+
var DEFAULT_MAX_VISIBLE = 3;
|
|
3783
|
+
function ListPicker({
|
|
3784
|
+
items,
|
|
3785
|
+
renderItem,
|
|
3786
|
+
onSelect,
|
|
3787
|
+
onCancel,
|
|
3788
|
+
maxVisible = DEFAULT_MAX_VISIBLE
|
|
3789
|
+
}) {
|
|
3790
|
+
const [state, setState] = useState13(() => createSelectionFlowState());
|
|
3791
|
+
const stateRef = useRef8(state);
|
|
3792
|
+
const applyAction = useCallback9(
|
|
3793
|
+
(action) => {
|
|
3794
|
+
const result = applySelectionInput(stateRef.current, action, {
|
|
3795
|
+
itemCount: items.length,
|
|
3796
|
+
maxVisible
|
|
3797
|
+
});
|
|
3798
|
+
stateRef.current = result.state;
|
|
3799
|
+
setState(result.state);
|
|
3800
|
+
if (result.effect.type === "cancel") {
|
|
3801
|
+
onCancel();
|
|
3802
|
+
} else if (result.effect.type === "select") {
|
|
3803
|
+
const item = items[result.effect.index];
|
|
3804
|
+
if (item !== void 0) {
|
|
3805
|
+
onSelect(item);
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
},
|
|
3809
|
+
[items, maxVisible, onCancel, onSelect]
|
|
3810
|
+
);
|
|
3811
|
+
useInput7((_input, key) => {
|
|
3812
|
+
const action = getVerticalSelectionInputAction(key);
|
|
3813
|
+
if (action !== void 0) {
|
|
3814
|
+
applyAction(action);
|
|
3815
|
+
}
|
|
3816
|
+
});
|
|
3817
|
+
if (items.length === 0) {
|
|
3818
|
+
return /* @__PURE__ */ jsx14(Box11, {});
|
|
3819
|
+
}
|
|
3820
|
+
const normalizedState = normalizeSelectionState(state, { itemCount: items.length, maxVisible });
|
|
3821
|
+
if (normalizedState !== state) {
|
|
3822
|
+
stateRef.current = normalizedState;
|
|
3823
|
+
}
|
|
3824
|
+
const { selectedIndex, scrollOffset } = normalizedState;
|
|
3825
|
+
const visibleItems = items.slice(scrollOffset, scrollOffset + maxVisible);
|
|
3826
|
+
const hasMore = scrollOffset + maxVisible < items.length;
|
|
3827
|
+
const hasLess = scrollOffset > 0;
|
|
3828
|
+
return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
|
|
3829
|
+
hasLess && /* @__PURE__ */ jsxs11(Text13, { dimColor: true, children: [
|
|
3830
|
+
" \u2191 ",
|
|
3831
|
+
scrollOffset,
|
|
3832
|
+
" more above"
|
|
3833
|
+
] }),
|
|
3834
|
+
visibleItems.map((item, index) => /* @__PURE__ */ jsx14(Box11, { marginBottom: 1, children: renderItem(item, scrollOffset + index === selectedIndex) }, scrollOffset + index)),
|
|
3835
|
+
hasMore && /* @__PURE__ */ jsxs11(Text13, { dimColor: true, children: [
|
|
3836
|
+
" \u2193 ",
|
|
3837
|
+
items.length - scrollOffset - maxVisible,
|
|
3838
|
+
" more below"
|
|
3839
|
+
] })
|
|
3840
|
+
] });
|
|
3841
|
+
}
|
|
3842
|
+
|
|
3843
|
+
// src/ui/SessionPicker.tsx
|
|
3844
|
+
import { Fragment as Fragment4, jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
3845
|
+
var SESSION_ID_DISPLAY_LENGTH = 8;
|
|
3846
|
+
function SessionPicker({
|
|
3847
|
+
sessionStore,
|
|
3848
|
+
cwd,
|
|
3849
|
+
onSelect,
|
|
3850
|
+
onCancel
|
|
3851
|
+
}) {
|
|
3852
|
+
const sessions = (sessionStore?.list() ?? []).filter((s) => s.cwd === cwd);
|
|
3853
|
+
return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
|
|
3854
|
+
/* @__PURE__ */ jsx15(Text14, { bold: true, color: "cyan", children: "Select a session to resume (ESC to cancel):" }),
|
|
3855
|
+
/* @__PURE__ */ jsx15(
|
|
3856
|
+
ListPicker,
|
|
3857
|
+
{
|
|
3858
|
+
items: sessions,
|
|
3859
|
+
renderItem: (session, isSelected) => {
|
|
3860
|
+
const lastMsg = session.messages.slice().reverse().find((m) => {
|
|
3861
|
+
const msg = m;
|
|
3862
|
+
return msg.role === "assistant" && msg.content;
|
|
3863
|
+
});
|
|
3864
|
+
const rawPreview = lastMsg?.content?.replace(/[\n\r]+/g, " ").trim() ?? "";
|
|
3865
|
+
const preview = rawPreview ? rawPreview.slice(0, 60) + (rawPreview.length > 60 ? "..." : "") : "";
|
|
3866
|
+
return /* @__PURE__ */ jsxs12(Text14, { children: [
|
|
3867
|
+
isSelected ? "> " : " ",
|
|
3868
|
+
/* @__PURE__ */ jsx15(Text14, { bold: true, children: session.name ?? session.id.slice(0, SESSION_ID_DISPLAY_LENGTH) }),
|
|
3869
|
+
" ",
|
|
3870
|
+
/* @__PURE__ */ jsx15(Text14, { dimColor: true, children: new Date(session.updatedAt).toLocaleString(void 0, {
|
|
3871
|
+
month: "short",
|
|
3872
|
+
day: "numeric",
|
|
3873
|
+
hour: "2-digit",
|
|
3874
|
+
minute: "2-digit"
|
|
3875
|
+
}) }),
|
|
3876
|
+
" ",
|
|
3877
|
+
/* @__PURE__ */ jsxs12(Text14, { dimColor: true, children: [
|
|
3878
|
+
"msgs: ",
|
|
3879
|
+
session.messages.length
|
|
3880
|
+
] }),
|
|
3881
|
+
preview ? /* @__PURE__ */ jsxs12(Fragment4, { children: [
|
|
3882
|
+
"\n ",
|
|
3883
|
+
/* @__PURE__ */ jsx15(Text14, { color: "gray", children: preview })
|
|
3884
|
+
] }) : null
|
|
3885
|
+
] });
|
|
3886
|
+
},
|
|
3887
|
+
onSelect: (session) => onSelect(session.id),
|
|
3888
|
+
onCancel
|
|
3889
|
+
}
|
|
3890
|
+
)
|
|
3891
|
+
] });
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
// src/ui/BackgroundTaskPanel.tsx
|
|
3895
|
+
import { Box as Box13, Text as Text15 } from "ink";
|
|
3896
|
+
import { jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
3897
|
+
var MS_PER_SECOND = 1e3;
|
|
3898
|
+
var SECONDS_PER_MINUTE = 60;
|
|
3899
|
+
var MINUTES_PER_HOUR = 60;
|
|
3900
|
+
function getStatusColor(status) {
|
|
3901
|
+
if (status === "completed") return "green";
|
|
3902
|
+
if (status === "failed") return "red";
|
|
3903
|
+
if (status === "cancelled") return "yellow";
|
|
3904
|
+
return "cyan";
|
|
3905
|
+
}
|
|
3906
|
+
function getStatusMarker(status) {
|
|
3907
|
+
if (status === "queued" || status === "running") return "\u25A1";
|
|
3908
|
+
return "\u25A0";
|
|
3909
|
+
}
|
|
3910
|
+
function getTaskPreview(task) {
|
|
3911
|
+
return task.errorPreview ?? task.resultPreview ?? task.currentAction ?? task.preview;
|
|
3912
|
+
}
|
|
3913
|
+
function formatAge(iso) {
|
|
3914
|
+
if (!iso) return void 0;
|
|
3915
|
+
const timestamp = Date.parse(iso);
|
|
3916
|
+
if (Number.isNaN(timestamp)) return void 0;
|
|
3917
|
+
const seconds = Math.max(0, Math.floor((Date.now() - timestamp) / MS_PER_SECOND));
|
|
3918
|
+
if (seconds < SECONDS_PER_MINUTE) return `${seconds}s`;
|
|
3919
|
+
const minutes = Math.floor(seconds / SECONDS_PER_MINUTE);
|
|
3920
|
+
if (minutes < MINUTES_PER_HOUR) return `${minutes}m`;
|
|
3921
|
+
return `${Math.floor(minutes / MINUTES_PER_HOUR)}h`;
|
|
3922
|
+
}
|
|
3923
|
+
function BackgroundTaskPanel({ tasks }) {
|
|
3924
|
+
if (tasks.length === 0) return null;
|
|
3925
|
+
return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", marginBottom: 1, children: [
|
|
3926
|
+
/* @__PURE__ */ jsx16(Text15, { color: "cyan", bold: true, children: "Background" }),
|
|
3927
|
+
tasks.map((task) => /* @__PURE__ */ jsxs13(Text15, { children: [
|
|
3928
|
+
"- ",
|
|
3929
|
+
/* @__PURE__ */ jsx16(Text15, { color: getStatusColor(task.status), children: getStatusMarker(task.status) }),
|
|
3930
|
+
` ${task.kind}:${task.label} ${task.id}`,
|
|
3931
|
+
task.status === "running" && formatAge(task.lastActivityAt) ? /* @__PURE__ */ jsx16(Text15, { dimColor: true, children: ` idle ${formatAge(task.lastActivityAt)}` }) : null,
|
|
3932
|
+
task.timeoutReason ? /* @__PURE__ */ jsx16(Text15, { dimColor: true, children: ` (${task.timeoutReason})` }) : null,
|
|
3933
|
+
getTaskPreview(task) ? /* @__PURE__ */ jsx16(Text15, { dimColor: true, children: ` - ${getTaskPreview(task)}` }) : null
|
|
3934
|
+
] }, task.id))
|
|
3935
|
+
] });
|
|
3936
|
+
}
|
|
3937
|
+
|
|
3938
|
+
// src/ui/App.tsx
|
|
3939
|
+
import { jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
3940
|
+
function App(props) {
|
|
3941
|
+
const [activeSessionId, setActiveSessionId] = useState14(props.resumeSessionId);
|
|
3942
|
+
return /* @__PURE__ */ jsx17(
|
|
3943
|
+
AppInner,
|
|
3944
|
+
{
|
|
3945
|
+
...props,
|
|
3946
|
+
resumeSessionId: activeSessionId,
|
|
3947
|
+
onSessionSwitch: (sessionId) => setActiveSessionId(sessionId)
|
|
3948
|
+
},
|
|
3949
|
+
activeSessionId ?? "__new__"
|
|
3950
|
+
);
|
|
3951
|
+
}
|
|
3952
|
+
function AppInner(props) {
|
|
3953
|
+
const cwd = props.cwd;
|
|
3954
|
+
const providerDefinitions = props.providerDefinitions ?? DEFAULT_PROVIDER_DEFINITIONS;
|
|
3955
|
+
const {
|
|
3956
|
+
interactiveSession,
|
|
3957
|
+
registry,
|
|
3958
|
+
history,
|
|
3959
|
+
addEntry,
|
|
3960
|
+
streamingText,
|
|
3961
|
+
activeTools,
|
|
3962
|
+
isThinking,
|
|
3963
|
+
isAborting,
|
|
3964
|
+
isShuttingDown,
|
|
3965
|
+
pendingPrompt,
|
|
3966
|
+
backgroundTasks,
|
|
3967
|
+
permissionRequest,
|
|
3968
|
+
contextState,
|
|
3969
|
+
handleSubmit: baseHandleSubmit,
|
|
3970
|
+
handleAbort,
|
|
3971
|
+
handleCancelQueue,
|
|
3972
|
+
handleShutdown
|
|
3973
|
+
} = useInteractiveSession({
|
|
3974
|
+
cwd,
|
|
3975
|
+
provider: props.provider,
|
|
3976
|
+
permissionMode: props.permissionMode,
|
|
3977
|
+
maxTurns: props.maxTurns,
|
|
3978
|
+
sessionStore: props.sessionStore,
|
|
3979
|
+
resumeSessionId: props.resumeSessionId,
|
|
3980
|
+
forkSession: props.forkSession,
|
|
3981
|
+
sessionName: props.sessionName,
|
|
3982
|
+
backgroundTaskRunners: props.backgroundTaskRunners,
|
|
3983
|
+
subagentRunnerFactory: props.subagentRunnerFactory,
|
|
3984
|
+
commandModules: props.commandModules,
|
|
3985
|
+
providerDefinitions
|
|
3986
|
+
});
|
|
3987
|
+
const pluginCallbacks = usePluginCallbacks(cwd);
|
|
3988
|
+
const { exit } = useApp2();
|
|
3989
|
+
const [sessionName, setSessionName] = useState14(props.sessionName);
|
|
3990
|
+
const {
|
|
3991
|
+
handleSubmit,
|
|
3992
|
+
pendingModelId,
|
|
3993
|
+
pendingProviderProfile,
|
|
3994
|
+
pendingProviderSetupType,
|
|
3995
|
+
showPluginTUI,
|
|
3996
|
+
showSessionPicker,
|
|
3997
|
+
setShowPluginTUI,
|
|
3998
|
+
setShowSessionPicker,
|
|
3999
|
+
handleModelConfirm,
|
|
4000
|
+
handleProviderConfirm,
|
|
4001
|
+
handleProviderSetupSubmit,
|
|
4002
|
+
handleProviderSetupCancel
|
|
4003
|
+
} = useSideEffects({
|
|
4004
|
+
cwd,
|
|
4005
|
+
interactiveSession,
|
|
4006
|
+
addEntry,
|
|
4007
|
+
baseHandleSubmit,
|
|
4008
|
+
setSessionName,
|
|
4009
|
+
providerDefinitions
|
|
4010
|
+
});
|
|
4011
|
+
useEffect4(() => {
|
|
4012
|
+
const name = interactiveSession?.getName?.();
|
|
4013
|
+
if (name && !sessionName) setSessionName(name);
|
|
4014
|
+
}, [interactiveSession, sessionName]);
|
|
4015
|
+
useEffect4(() => {
|
|
4016
|
+
const title = sessionName ? `Robota \u2014 ${sessionName}` : "Robota";
|
|
4017
|
+
process.stdout.write(`\x1B]0;${title}\x07`);
|
|
4018
|
+
}, [sessionName]);
|
|
4019
|
+
useInput8((_input, key) => {
|
|
4020
|
+
if (!key.escape || !isThinking) return;
|
|
4021
|
+
if (permissionRequest || showPluginTUI || showSessionPicker) return;
|
|
4022
|
+
handleAbort();
|
|
4023
|
+
});
|
|
4024
|
+
useInput8((input, key) => {
|
|
4025
|
+
if (!key.ctrl || input !== "c" || isShuttingDown) return;
|
|
4026
|
+
void handleShutdown("prompt_input_exit").finally(() => exit());
|
|
4027
|
+
});
|
|
4028
|
+
useEffect4(() => {
|
|
4029
|
+
const onSigterm = () => {
|
|
4030
|
+
if (isShuttingDown) return;
|
|
4031
|
+
void handleShutdown("other").finally(() => exit());
|
|
4032
|
+
};
|
|
4033
|
+
process.once("SIGINT", onSigterm);
|
|
4034
|
+
process.once("SIGTERM", onSigterm);
|
|
4035
|
+
return () => {
|
|
4036
|
+
process.off("SIGINT", onSigterm);
|
|
4037
|
+
process.off("SIGTERM", onSigterm);
|
|
4038
|
+
};
|
|
4039
|
+
}, [handleShutdown, exit, isShuttingDown]);
|
|
4040
|
+
let permissionMode = props.permissionMode ?? "default";
|
|
4041
|
+
let sessionId = "";
|
|
4042
|
+
try {
|
|
4043
|
+
const session = interactiveSession.getSession();
|
|
4044
|
+
permissionMode = session.getPermissionMode();
|
|
4045
|
+
sessionId = session.getSessionId();
|
|
4046
|
+
} catch {
|
|
4047
|
+
}
|
|
4048
|
+
return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", children: [
|
|
4049
|
+
/* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
|
|
4050
|
+
/* @__PURE__ */ jsx17(Text16, { color: "cyan", bold: true, children: `
|
|
4051
|
+
____ ___ ____ ___ _____ _
|
|
4052
|
+
| _ \\ / _ \\| __ ) / _ \\_ _|/ \\
|
|
4053
|
+
| |_) | | | | _ \\| | | || | / _ \\
|
|
4054
|
+
| _ <| |_| | |_) | |_| || |/ ___ \\
|
|
4055
|
+
|_| \\_\\\\___/|____/ \\___/ |_/_/ \\_\\
|
|
4056
|
+
` }),
|
|
4057
|
+
/* @__PURE__ */ jsxs14(Text16, { dimColor: true, children: [
|
|
4058
|
+
" v",
|
|
4059
|
+
props.version ?? "0.0.0"
|
|
4060
|
+
] })
|
|
4061
|
+
] }),
|
|
4062
|
+
/* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: [
|
|
4063
|
+
/* @__PURE__ */ jsx17(MessageList, { history }),
|
|
4064
|
+
isShuttingDown && /* @__PURE__ */ jsx17(Box14, { marginBottom: 1, children: /* @__PURE__ */ jsx17(Text16, { color: "yellow", children: "Shutting down..." }) }),
|
|
4065
|
+
(isThinking || activeTools.length > 0) && /* @__PURE__ */ jsx17(Box14, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsx17(StreamingIndicator, { text: streamingText, activeTools }) }),
|
|
4066
|
+
/* @__PURE__ */ jsx17(BackgroundTaskPanel, { tasks: backgroundTasks })
|
|
4067
|
+
] }),
|
|
4068
|
+
permissionRequest && /* @__PURE__ */ jsx17(PermissionPrompt, { request: permissionRequest }),
|
|
4069
|
+
pendingModelId && /* @__PURE__ */ jsx17(
|
|
4070
|
+
ConfirmPrompt,
|
|
4071
|
+
{
|
|
4072
|
+
message: `Change model to ${getModelName2(pendingModelId)}? This will restart the session.`,
|
|
4073
|
+
onSelect: handleModelConfirm
|
|
4074
|
+
}
|
|
4075
|
+
),
|
|
4076
|
+
pendingProviderProfile && /* @__PURE__ */ jsx17(
|
|
4077
|
+
ConfirmPrompt,
|
|
4078
|
+
{
|
|
4079
|
+
message: `Change provider to ${pendingProviderProfile}? This will restart the session.`,
|
|
4080
|
+
onSelect: handleProviderConfirm
|
|
4081
|
+
}
|
|
4082
|
+
),
|
|
4083
|
+
pendingProviderSetupType && /* @__PURE__ */ jsx17(
|
|
4084
|
+
ProviderSetupPrompt,
|
|
4085
|
+
{
|
|
4086
|
+
type: pendingProviderSetupType,
|
|
4087
|
+
providerDefinitions,
|
|
4088
|
+
onSubmit: handleProviderSetupSubmit,
|
|
4089
|
+
onCancel: handleProviderSetupCancel
|
|
4090
|
+
}
|
|
4091
|
+
),
|
|
4092
|
+
showPluginTUI && /* @__PURE__ */ jsx17(
|
|
4093
|
+
PluginTUI,
|
|
4094
|
+
{
|
|
4095
|
+
callbacks: pluginCallbacks,
|
|
4096
|
+
onClose: () => setShowPluginTUI(false),
|
|
4097
|
+
addMessage: (msg) => addEntry(messageToHistoryEntry4(createSystemMessage4(msg.content)))
|
|
4098
|
+
}
|
|
4099
|
+
),
|
|
4100
|
+
showSessionPicker && /* @__PURE__ */ jsx17(
|
|
4101
|
+
SessionPicker,
|
|
4102
|
+
{
|
|
4103
|
+
sessionStore: props.sessionStore,
|
|
4104
|
+
cwd: props.cwd,
|
|
4105
|
+
onSelect: (id) => {
|
|
4106
|
+
setShowSessionPicker(false);
|
|
4107
|
+
props.onSessionSwitch(id);
|
|
4108
|
+
},
|
|
4109
|
+
onCancel: () => {
|
|
4110
|
+
setShowSessionPicker(false);
|
|
4111
|
+
addEntry(messageToHistoryEntry4(createSystemMessage4("Session resume cancelled.")));
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
4114
|
+
),
|
|
4115
|
+
/* @__PURE__ */ jsx17(
|
|
4116
|
+
StatusBar,
|
|
4117
|
+
{
|
|
4118
|
+
permissionMode,
|
|
4119
|
+
modelName: props.modelId ? getModelName2(props.modelId) : "",
|
|
4120
|
+
sessionId,
|
|
4121
|
+
messageCount: history.length,
|
|
4122
|
+
isThinking,
|
|
4123
|
+
contextPercentage: contextState.percentage,
|
|
4124
|
+
contextUsedTokens: contextState.usedTokens,
|
|
4125
|
+
contextMaxTokens: contextState.maxTokens,
|
|
4126
|
+
sessionName
|
|
4127
|
+
}
|
|
4128
|
+
),
|
|
4129
|
+
/* @__PURE__ */ jsx17(
|
|
4130
|
+
InputArea,
|
|
4131
|
+
{
|
|
4132
|
+
onSubmit: handleSubmit,
|
|
4133
|
+
onCancelQueue: handleCancelQueue,
|
|
4134
|
+
isDisabled: !!permissionRequest || showPluginTUI || showSessionPicker || isShuttingDown || !!pendingProviderSetupType || isThinking && !!pendingPrompt,
|
|
4135
|
+
isAborting,
|
|
4136
|
+
pendingPrompt,
|
|
4137
|
+
registry,
|
|
4138
|
+
sessionName
|
|
4139
|
+
}
|
|
4140
|
+
),
|
|
4141
|
+
/* @__PURE__ */ jsx17(Text16, { children: " " })
|
|
4142
|
+
] });
|
|
4143
|
+
}
|
|
4144
|
+
|
|
4145
|
+
// src/ui/render.tsx
|
|
4146
|
+
import { jsx as jsx18 } from "react/jsx-runtime";
|
|
4147
|
+
function renderApp(options) {
|
|
4148
|
+
process.on("unhandledRejection", (reason) => {
|
|
4149
|
+
process.stderr.write(`
|
|
4150
|
+
[UNHANDLED REJECTION] ${reason}
|
|
4151
|
+
`);
|
|
4152
|
+
if (reason instanceof Error) {
|
|
4153
|
+
process.stderr.write(`${reason.stack}
|
|
4154
|
+
`);
|
|
4155
|
+
}
|
|
4156
|
+
});
|
|
4157
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
4158
|
+
process.stdout.write("\x1B[?2004h");
|
|
4159
|
+
}
|
|
4160
|
+
const instance = render(/* @__PURE__ */ jsx18(App, { ...options }), {
|
|
4161
|
+
exitOnCtrlC: false
|
|
4162
|
+
});
|
|
4163
|
+
instance.waitUntilExit().then(() => {
|
|
4164
|
+
if (process.stdout.isTTY) {
|
|
4165
|
+
process.stdout.write("\x1B[?2004l");
|
|
4166
|
+
}
|
|
4167
|
+
process.exit(0);
|
|
4168
|
+
}).catch((err) => {
|
|
4169
|
+
if (err) {
|
|
4170
|
+
process.stderr.write(`
|
|
4171
|
+
[EXIT ERROR] ${err}
|
|
4172
|
+
`);
|
|
4173
|
+
}
|
|
4174
|
+
process.exit(1);
|
|
4175
|
+
});
|
|
4176
|
+
}
|
|
4177
|
+
|
|
4178
|
+
// src/cli.ts
|
|
4179
|
+
function readVersion() {
|
|
4180
|
+
try {
|
|
4181
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
4182
|
+
const dir = dirname3(thisFile);
|
|
4183
|
+
const candidates = [join7(dir, "..", "..", "package.json"), join7(dir, "..", "package.json")];
|
|
4184
|
+
for (const pkgPath of candidates) {
|
|
4185
|
+
try {
|
|
4186
|
+
const raw = readFileSync4(pkgPath, "utf-8");
|
|
4187
|
+
const pkg = JSON.parse(raw);
|
|
4188
|
+
if (pkg.version !== void 0 && pkg.name !== void 0) {
|
|
4189
|
+
return pkg.version;
|
|
4190
|
+
}
|
|
4191
|
+
} catch {
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
return "0.0.0";
|
|
4195
|
+
} catch {
|
|
4196
|
+
return "0.0.0";
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
function promptInput(label, masked = false) {
|
|
4200
|
+
return new Promise((resolve) => {
|
|
4201
|
+
process.stdout.write(label);
|
|
4202
|
+
let input = "";
|
|
4203
|
+
const stdin = process.stdin;
|
|
4204
|
+
const wasRaw = stdin.isRaw;
|
|
4205
|
+
stdin.setRawMode(true);
|
|
4206
|
+
stdin.resume();
|
|
4207
|
+
stdin.setEncoding("utf8");
|
|
4208
|
+
const onData = (data) => {
|
|
4209
|
+
for (const ch of data) {
|
|
4210
|
+
if (ch === "\r" || ch === "\n") {
|
|
4211
|
+
stdin.removeListener("data", onData);
|
|
4212
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
4213
|
+
stdin.pause();
|
|
4214
|
+
process.stdout.write("\n");
|
|
4215
|
+
resolve(input.trim());
|
|
4216
|
+
return;
|
|
4217
|
+
} else if (ch === "\x7F" || ch === "\b") {
|
|
4218
|
+
if (input.length > 0) {
|
|
4219
|
+
input = input.slice(0, -1);
|
|
4220
|
+
process.stdout.write("\b \b");
|
|
4221
|
+
}
|
|
4222
|
+
} else if (ch === "") {
|
|
4223
|
+
process.stdout.write("\n");
|
|
4224
|
+
process.exit(0);
|
|
4225
|
+
} else if (ch.charCodeAt(0) >= 32) {
|
|
4226
|
+
input += ch;
|
|
4227
|
+
process.stdout.write(masked ? "*" : ch);
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
4230
|
+
};
|
|
4231
|
+
stdin.on("data", onData);
|
|
4232
|
+
});
|
|
4233
|
+
}
|
|
4234
|
+
function resetConfig() {
|
|
4235
|
+
const userPath = getUserSettingsPath();
|
|
4236
|
+
if (deleteSettings(userPath)) {
|
|
4237
|
+
process.stdout.write(`Deleted ${userPath}
|
|
4238
|
+
`);
|
|
4239
|
+
} else {
|
|
4240
|
+
process.stdout.write("No user settings found.\n");
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
async function startCli(options = {}) {
|
|
4244
|
+
const args = parseCliArgs();
|
|
4245
|
+
if (args.version) {
|
|
4246
|
+
process.stdout.write(`robota ${readVersion()}
|
|
4247
|
+
`);
|
|
4248
|
+
return;
|
|
4249
|
+
}
|
|
4250
|
+
if (args.reset) {
|
|
4251
|
+
resetConfig();
|
|
4252
|
+
return;
|
|
4253
|
+
}
|
|
4254
|
+
const cwd = process.cwd();
|
|
4255
|
+
const providerDefinitions = options.providerDefinitions ?? DEFAULT_PROVIDER_DEFINITIONS;
|
|
4256
|
+
if (args.configure) {
|
|
4257
|
+
await runInteractiveProviderSetup(cwd, args, promptInput, providerDefinitions);
|
|
4258
|
+
return;
|
|
4259
|
+
}
|
|
4260
|
+
if (handleProviderConfigurationArgs(cwd, args, providerDefinitions)) {
|
|
4261
|
+
return;
|
|
4262
|
+
}
|
|
4263
|
+
try {
|
|
4264
|
+
await ensureConfig(cwd, args, promptInput, providerDefinitions);
|
|
4265
|
+
} catch (error) {
|
|
4266
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}
|
|
4267
|
+
`);
|
|
4268
|
+
process.exit(1);
|
|
4269
|
+
}
|
|
4270
|
+
const providerOptions = args.provider ? { providerOverride: args.provider, providerDefinitions } : { providerDefinitions };
|
|
4271
|
+
const providerSettings = readProviderSettings(cwd, providerOptions);
|
|
4272
|
+
const modelId = args.model ?? providerSettings.model;
|
|
4273
|
+
const provider = createProviderFromSettings(cwd, args.model, providerOptions);
|
|
4274
|
+
const backgroundTaskRunners = [createManagedShellProcessRunner()];
|
|
4275
|
+
const paths = projectPaths(cwd);
|
|
4276
|
+
const subagentRunnerFactory = createChildProcessSubagentRunnerFactory({
|
|
4277
|
+
providerConfig: { ...providerSettings, model: modelId },
|
|
4278
|
+
logsDir: paths.logs
|
|
4279
|
+
});
|
|
4280
|
+
const sessionStore = new SessionStore(paths.sessions);
|
|
4281
|
+
let resumeSessionId;
|
|
4282
|
+
if (args.continueMode) {
|
|
4283
|
+
const sessions = sessionStore.list().filter((s) => s.cwd === cwd);
|
|
4284
|
+
if (sessions.length > 0) {
|
|
4285
|
+
resumeSessionId = sessions[0].id;
|
|
4286
|
+
}
|
|
4287
|
+
} else if (args.resumeId !== void 0) {
|
|
4288
|
+
if (args.resumeId === "") {
|
|
4289
|
+
resumeSessionId = "__picker__";
|
|
4290
|
+
} else {
|
|
4291
|
+
const sessions = sessionStore.list();
|
|
4292
|
+
const match = sessions.find((s) => s.id === args.resumeId || s.name === args.resumeId);
|
|
4293
|
+
if (match) {
|
|
4294
|
+
resumeSessionId = match.id;
|
|
4295
|
+
} else {
|
|
4296
|
+
process.stderr.write(`Session not found: ${args.resumeId}
|
|
4297
|
+
`);
|
|
4298
|
+
process.exit(1);
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
if (args.printMode) {
|
|
4303
|
+
let prompt = args.positional.join(" ").trim();
|
|
4304
|
+
if (!prompt && !process.stdin.isTTY) {
|
|
4305
|
+
const chunks = [];
|
|
4306
|
+
for await (const chunk of process.stdin) {
|
|
4307
|
+
chunks.push(chunk);
|
|
4308
|
+
}
|
|
4309
|
+
prompt = Buffer.concat(chunks).toString("utf-8").trim();
|
|
4310
|
+
}
|
|
4311
|
+
if (!prompt) {
|
|
4312
|
+
process.stderr.write("Print mode (-p) requires a prompt argument.\n");
|
|
4313
|
+
process.exit(1);
|
|
4314
|
+
}
|
|
4315
|
+
const appendParts = [];
|
|
4316
|
+
if (args.appendSystemPrompt) appendParts.push(args.appendSystemPrompt);
|
|
4317
|
+
if (args.jsonSchema)
|
|
4318
|
+
appendParts.push(
|
|
4319
|
+
`Respond with valid JSON only, matching this JSON schema:
|
|
4320
|
+
${args.jsonSchema}`
|
|
4321
|
+
);
|
|
4322
|
+
const appendSystemPrompt = appendParts.length > 0 ? appendParts.join("\n\n") : void 0;
|
|
4323
|
+
const session = new InteractiveSession2({
|
|
4324
|
+
cwd,
|
|
4325
|
+
provider,
|
|
4326
|
+
permissionMode: args.permissionMode ?? "bypassPermissions",
|
|
4327
|
+
maxTurns: args.maxTurns,
|
|
4328
|
+
sessionStore: args.noSessionPersistence ? void 0 : sessionStore,
|
|
4329
|
+
sessionName: args.sessionName,
|
|
4330
|
+
bare: args.bare || void 0,
|
|
4331
|
+
allowedTools: args.allowedTools ? args.allowedTools.split(",").map((t) => t.trim()).filter((t) => t.length > 0) : void 0,
|
|
4332
|
+
appendSystemPrompt,
|
|
4333
|
+
backgroundTaskRunners,
|
|
4334
|
+
subagentRunnerFactory,
|
|
4335
|
+
commandModules: options.commandModules
|
|
4336
|
+
});
|
|
4337
|
+
const transport = createHeadlessTransport({
|
|
4338
|
+
outputFormat: args.outputFormat ?? "text",
|
|
4339
|
+
prompt
|
|
4340
|
+
});
|
|
4341
|
+
session.attachTransport(transport);
|
|
4342
|
+
await transport.start();
|
|
4343
|
+
await session.shutdown({ reason: "prompt_input_exit", message: "Headless transport complete" });
|
|
4344
|
+
process.exit(transport.getExitCode());
|
|
4345
|
+
}
|
|
4346
|
+
renderApp({
|
|
4347
|
+
cwd,
|
|
4348
|
+
provider,
|
|
4349
|
+
modelId,
|
|
4350
|
+
language: args.language,
|
|
4351
|
+
permissionMode: args.permissionMode,
|
|
4352
|
+
maxTurns: args.maxTurns,
|
|
4353
|
+
version: readVersion(),
|
|
4354
|
+
sessionStore,
|
|
4355
|
+
resumeSessionId,
|
|
4356
|
+
forkSession: args.forkSession,
|
|
4357
|
+
sessionName: args.sessionName,
|
|
4358
|
+
backgroundTaskRunners,
|
|
4359
|
+
subagentRunnerFactory,
|
|
4360
|
+
commandModules: options.commandModules,
|
|
4361
|
+
providerDefinitions
|
|
4362
|
+
});
|
|
4363
|
+
}
|
|
4364
|
+
|
|
4365
|
+
export {
|
|
4366
|
+
createManagedShellProcessRunner,
|
|
4367
|
+
createGitWorktreeIsolationAdapter,
|
|
4368
|
+
GitWorktreeIsolationAdapter,
|
|
4369
|
+
createChildProcessSubagentRunnerFactory,
|
|
4370
|
+
ChildProcessSubagentRunner,
|
|
4371
|
+
startCli
|
|
4372
|
+
};
|