@pickle-pee/genesis-cli 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -0
- package/dist/bootstrap.d.ts +18 -0
- package/dist/bootstrap.js +89 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/input-loop.d.ts +42 -0
- package/dist/input-loop.js +344 -0
- package/dist/input-loop.js.map +1 -0
- package/dist/main.d.ts +35 -0
- package/dist/main.js +321 -0
- package/dist/main.js.map +1 -0
- package/dist/mode-dispatch.d.ts +100 -0
- package/dist/mode-dispatch.js +1830 -0
- package/dist/mode-dispatch.js.map +1 -0
- package/dist/rpc-server.d.ts +16 -0
- package/dist/rpc-server.js +339 -0
- package/dist/rpc-server.js.map +1 -0
- package/dist/session-store.d.ts +12 -0
- package/dist/session-store.js +48 -0
- package/dist/session-store.js.map +1 -0
- package/dist/terminal-display-width.d.ts +1 -0
- package/dist/terminal-display-width.js +33 -0
- package/dist/terminal-display-width.js.map +1 -0
- package/dist/test/bootstrap.test.d.ts +1 -0
- package/dist/test/bootstrap.test.js +50 -0
- package/dist/test/bootstrap.test.js.map +1 -0
- package/dist/test/input-loop-raw.test.d.ts +1 -0
- package/dist/test/input-loop-raw.test.js +204 -0
- package/dist/test/input-loop-raw.test.js.map +1 -0
- package/dist/test/interactive-tty-workbench.test.d.ts +1 -0
- package/dist/test/interactive-tty-workbench.test.js +647 -0
- package/dist/test/interactive-tty-workbench.test.js.map +1 -0
- package/dist/test/main.test.d.ts +1 -0
- package/dist/test/main.test.js +42 -0
- package/dist/test/main.test.js.map +1 -0
- package/dist/test/mode-dispatch.test.d.ts +1 -0
- package/dist/test/mode-dispatch.test.js +315 -0
- package/dist/test/mode-dispatch.test.js.map +1 -0
- package/dist/test/permission-flow.test.d.ts +7 -0
- package/dist/test/permission-flow.test.js +191 -0
- package/dist/test/permission-flow.test.js.map +1 -0
- package/dist/test/rpc-server.test.d.ts +7 -0
- package/dist/test/rpc-server.test.js +285 -0
- package/dist/test/rpc-server.test.js.map +1 -0
- package/dist/test/session-store.test.d.ts +1 -0
- package/dist/test/session-store.test.js +57 -0
- package/dist/test/session-store.test.js.map +1 -0
- package/dist/test/terminal-display-width.test.d.ts +1 -0
- package/dist/test/terminal-display-width.test.js +25 -0
- package/dist/test/terminal-display-width.test.js.map +1 -0
- package/dist/test/tty-session.test.d.ts +1 -0
- package/dist/test/tty-session.test.js +114 -0
- package/dist/test/tty-session.test.js.map +1 -0
- package/dist/theme.d.ts +16 -0
- package/dist/theme.js +20 -0
- package/dist/theme.js.map +1 -0
- package/dist/tty-session.d.ts +26 -0
- package/dist/tty-session.js +116 -0
- package/dist/tty-session.js.map +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +26 -0
|
@@ -0,0 +1,1830 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Mode dispatch — four CLI mode handlers sharing the same AppRuntime.
|
|
4
|
+
*
|
|
5
|
+
* Each mode handler receives an identical AppRuntime and creates sessions
|
|
6
|
+
* from it. Mode-specific behavior is isolated to how events are rendered.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.WELCOME_BIBLE_GREETINGS = void 0;
|
|
10
|
+
exports.createModeHandler = createModeHandler;
|
|
11
|
+
exports.pickWelcomeGreeting = pickWelcomeGreeting;
|
|
12
|
+
exports.buildWelcomeLines = buildWelcomeLines;
|
|
13
|
+
exports.formatWelcomeTopBorder = formatWelcomeTopBorder;
|
|
14
|
+
exports.formatWelcomeBottomBorder = formatWelcomeBottomBorder;
|
|
15
|
+
exports.formatWelcomeFilledLine = formatWelcomeFilledLine;
|
|
16
|
+
exports.formatWelcomeCenteredLine = formatWelcomeCenteredLine;
|
|
17
|
+
exports.computePromptCursorColumn = computePromptCursorColumn;
|
|
18
|
+
exports.shouldRenderInteractiveTranscriptEvent = shouldRenderInteractiveTranscriptEvent;
|
|
19
|
+
exports.formatInteractiveToolEvent = formatInteractiveToolEvent;
|
|
20
|
+
exports.formatInteractivePermissionBlock = formatInteractivePermissionBlock;
|
|
21
|
+
exports.formatInteractiveFooter = formatInteractiveFooter;
|
|
22
|
+
exports.movePermissionSelection = movePermissionSelection;
|
|
23
|
+
exports.permissionDecisionFromSelection = permissionDecisionFromSelection;
|
|
24
|
+
exports.formatInteractiveToolTitle = formatInteractiveToolTitle;
|
|
25
|
+
exports.formatInteractiveToolResult = formatInteractiveToolResult;
|
|
26
|
+
exports.computeSlashSuggestions = computeSlashSuggestions;
|
|
27
|
+
exports.formatSlashSuggestionHint = formatSlashSuggestionHint;
|
|
28
|
+
exports.acceptFirstSlashSuggestion = acceptFirstSlashSuggestion;
|
|
29
|
+
exports.formatTranscriptUserLine = formatTranscriptUserLine;
|
|
30
|
+
exports.formatTranscriptAssistantLine = formatTranscriptAssistantLine;
|
|
31
|
+
exports.formatInteractivePromptBuffer = formatInteractivePromptBuffer;
|
|
32
|
+
exports.formatInteractiveInputSeparator = formatInteractiveInputSeparator;
|
|
33
|
+
exports.computeInteractiveFooterSeparatorWidth = computeInteractiveFooterSeparatorWidth;
|
|
34
|
+
exports.computeFooterCursorColumn = computeFooterCursorColumn;
|
|
35
|
+
exports.countRenderedTerminalRows = countRenderedTerminalRows;
|
|
36
|
+
exports.computePromptCursorRowsUp = computePromptCursorRowsUp;
|
|
37
|
+
exports.computeFooterCursorRowsUp = computeFooterCursorRowsUp;
|
|
38
|
+
exports.computeFooterCursorRowsFromEnd = computeFooterCursorRowsFromEnd;
|
|
39
|
+
exports.computeInteractiveEphemeralRows = computeInteractiveEphemeralRows;
|
|
40
|
+
exports.fitTerminalLine = fitTerminalLine;
|
|
41
|
+
exports.formatTurnNotice = formatTurnNotice;
|
|
42
|
+
exports.mergeStreamingText = mergeStreamingText;
|
|
43
|
+
exports.wrapTranscriptContent = wrapTranscriptContent;
|
|
44
|
+
exports.computeVisibleTranscriptLines = computeVisibleTranscriptLines;
|
|
45
|
+
exports.computeTranscriptDisplayRows = computeTranscriptDisplayRows;
|
|
46
|
+
exports.materializeAssistantTranscriptBlock = materializeAssistantTranscriptBlock;
|
|
47
|
+
exports.appendAssistantTranscriptBlock = appendAssistantTranscriptBlock;
|
|
48
|
+
exports.computeFooterStartRow = computeFooterStartRow;
|
|
49
|
+
const node_child_process_1 = require("node:child_process");
|
|
50
|
+
const promises_1 = require("node:fs/promises");
|
|
51
|
+
const node_path_1 = require("node:path");
|
|
52
|
+
const ui_1 = require("@pickle-pee/ui");
|
|
53
|
+
const input_loop_js_1 = require("./input-loop.js");
|
|
54
|
+
const rpc_server_js_1 = require("./rpc-server.js");
|
|
55
|
+
const session_store_js_1 = require("./session-store.js");
|
|
56
|
+
const terminal_display_width_js_1 = require("./terminal-display-width.js");
|
|
57
|
+
const theme_js_1 = require("./theme.js");
|
|
58
|
+
const tty_session_js_1 = require("./tty-session.js");
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Factory
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
function createModeHandler(mode) {
|
|
63
|
+
switch (mode) {
|
|
64
|
+
case "interactive":
|
|
65
|
+
return new InteractiveModeHandler();
|
|
66
|
+
case "print":
|
|
67
|
+
return new PrintModeHandler();
|
|
68
|
+
case "json":
|
|
69
|
+
return new JsonModeHandler();
|
|
70
|
+
case "rpc":
|
|
71
|
+
return new RpcModeHandler();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Interactive mode
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
class InteractiveModeHandler {
|
|
78
|
+
_pendingPermissionCallId = null;
|
|
79
|
+
_pendingPermissionDetails = null;
|
|
80
|
+
_activeTurn = null;
|
|
81
|
+
_prompt = "❯ ";
|
|
82
|
+
_inputState = { buffer: "", cursor: 0 };
|
|
83
|
+
_history = [];
|
|
84
|
+
_historyIndex = null;
|
|
85
|
+
_suppressPersistOnce = false;
|
|
86
|
+
_lastError = null;
|
|
87
|
+
_changedPaths = new Set();
|
|
88
|
+
_transcriptBlocks = [];
|
|
89
|
+
_assistantBuffer = "";
|
|
90
|
+
_streamingReservedRows = 0;
|
|
91
|
+
_streamingDisplayRows = 0;
|
|
92
|
+
_renderedStreamingStartRow = null;
|
|
93
|
+
_turnNotice = null;
|
|
94
|
+
_commandSuggestions = [];
|
|
95
|
+
_toolCalls = new Map();
|
|
96
|
+
_queuedInputs = [];
|
|
97
|
+
_pendingPermissionSelection = 0;
|
|
98
|
+
_renderedFooterUi = null;
|
|
99
|
+
_renderedFooterStartRow = null;
|
|
100
|
+
_welcomeLines = [];
|
|
101
|
+
async start(runtime) {
|
|
102
|
+
const handler = this;
|
|
103
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
104
|
+
throw new Error("Interactive mode requires a TTY. Use --mode print|json|rpc instead.");
|
|
105
|
+
}
|
|
106
|
+
const sessionRef = { current: runtime.createSession() };
|
|
107
|
+
const sink = {
|
|
108
|
+
write: (text) => {
|
|
109
|
+
this.writeTranscriptText(text, false);
|
|
110
|
+
},
|
|
111
|
+
writeLine: (text) => {
|
|
112
|
+
this.writeTranscriptText(text, true);
|
|
113
|
+
},
|
|
114
|
+
writeError: (text) => {
|
|
115
|
+
this.writeTranscriptText(`Error: ${text}`, true);
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
// Slash command registry
|
|
119
|
+
const registry = (0, ui_1.createSlashCommandRegistry)();
|
|
120
|
+
for (const cmd of (0, ui_1.createBuiltinCommands)()) {
|
|
121
|
+
registry.register(cmd);
|
|
122
|
+
}
|
|
123
|
+
let interactionState = (0, ui_1.initialInteractionState)();
|
|
124
|
+
let sessionTitle;
|
|
125
|
+
let exitRequested = false;
|
|
126
|
+
let inputLoop = null;
|
|
127
|
+
const ttySession = (0, tty_session_js_1.createTtySession)({
|
|
128
|
+
onResume: () => {
|
|
129
|
+
this.rerenderInteractiveRegions();
|
|
130
|
+
},
|
|
131
|
+
useAlternateScreen: false,
|
|
132
|
+
enableMouseTracking: false,
|
|
133
|
+
});
|
|
134
|
+
const onResize = () => {
|
|
135
|
+
this.rerenderInteractiveRegions();
|
|
136
|
+
};
|
|
137
|
+
process.stdout.on("resize", onResize);
|
|
138
|
+
const resolveAgentDir = () => {
|
|
139
|
+
return (sessionRef.current.context.agentDir ??
|
|
140
|
+
(0, node_path_1.join)(sessionRef.current.context.workingDirectory, ".genesis-local", "pi-agent"));
|
|
141
|
+
};
|
|
142
|
+
const attachSession = (next) => {
|
|
143
|
+
sessionRef.current.events.removeAllListeners();
|
|
144
|
+
sessionRef.current = next;
|
|
145
|
+
this._pendingPermissionCallId = null;
|
|
146
|
+
this._pendingPermissionDetails = null;
|
|
147
|
+
this._activeTurn = null;
|
|
148
|
+
this._historyIndex = null;
|
|
149
|
+
this._lastError = null;
|
|
150
|
+
this._changedPaths.clear();
|
|
151
|
+
this._transcriptBlocks.length = 0;
|
|
152
|
+
this._assistantBuffer = "";
|
|
153
|
+
this._streamingReservedRows = 0;
|
|
154
|
+
this._streamingDisplayRows = 0;
|
|
155
|
+
this._renderedStreamingStartRow = null;
|
|
156
|
+
this._turnNotice = null;
|
|
157
|
+
this._commandSuggestions = [];
|
|
158
|
+
this._toolCalls.clear();
|
|
159
|
+
this._queuedInputs.length = 0;
|
|
160
|
+
this._pendingPermissionSelection = 0;
|
|
161
|
+
this._renderedFooterUi = null;
|
|
162
|
+
this._renderedFooterStartRow = null;
|
|
163
|
+
sessionTitle = undefined;
|
|
164
|
+
interactionState = (0, ui_1.initialInteractionState)();
|
|
165
|
+
sessionRef.current.events.on("session_closed", (event) => {
|
|
166
|
+
if (this._suppressPersistOnce) {
|
|
167
|
+
this._suppressPersistOnce = false;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const dir = (0, session_store_js_1.getSessionStoreDir)(resolveAgentDir());
|
|
172
|
+
void (0, session_store_js_1.writeLastSession)(dir, event.recoveryData, { title: sessionTitle });
|
|
173
|
+
}
|
|
174
|
+
catch { }
|
|
175
|
+
});
|
|
176
|
+
sessionRef.current.events.onAny((event) => {
|
|
177
|
+
if (event.type === "permission_requested") {
|
|
178
|
+
this._pendingPermissionDetails = {
|
|
179
|
+
toolName: event.toolName,
|
|
180
|
+
toolCallId: event.toolCallId,
|
|
181
|
+
riskLevel: event.riskLevel,
|
|
182
|
+
reason: event.reason,
|
|
183
|
+
targetPath: event.targetPath,
|
|
184
|
+
};
|
|
185
|
+
this._pendingPermissionSelection = 0;
|
|
186
|
+
}
|
|
187
|
+
if (event.type === "permission_resolved") {
|
|
188
|
+
if (this._pendingPermissionDetails?.toolCallId === event.toolCallId) {
|
|
189
|
+
this._pendingPermissionDetails = null;
|
|
190
|
+
this._pendingPermissionSelection = 0;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (event.type === "tool_started") {
|
|
194
|
+
this._toolCalls.set(event.toolCallId, { toolName: event.toolName, parameters: event.parameters });
|
|
195
|
+
const targetPath = typeof event.parameters.file_path === "string"
|
|
196
|
+
? event.parameters.file_path
|
|
197
|
+
: typeof event.parameters.path === "string"
|
|
198
|
+
? event.parameters.path
|
|
199
|
+
: undefined;
|
|
200
|
+
if (targetPath && (event.toolName === "edit" || event.toolName === "write")) {
|
|
201
|
+
this._changedPaths.add(targetPath);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (event.type === "tool_denied") {
|
|
205
|
+
this._toolCalls.delete(event.toolCallId);
|
|
206
|
+
this._lastError = `${event.toolName}: ${event.reason}`;
|
|
207
|
+
}
|
|
208
|
+
if (event.type === "tool_completed" && event.status === "failure") {
|
|
209
|
+
this._toolCalls.delete(event.toolCallId);
|
|
210
|
+
this._lastError = `${event.toolName}: ${event.result ?? "failure"}`;
|
|
211
|
+
}
|
|
212
|
+
if (event.type === "tool_completed" && event.status === "success") {
|
|
213
|
+
this._toolCalls.delete(event.toolCallId);
|
|
214
|
+
}
|
|
215
|
+
interactionState = (0, ui_1.reduceInteractionState)(interactionState, event);
|
|
216
|
+
if (interactionState.phase === "waiting_permission" && interactionState.activeToolCallId) {
|
|
217
|
+
this._pendingPermissionCallId = interactionState.activeToolCallId;
|
|
218
|
+
}
|
|
219
|
+
else if (interactionState.phase !== "waiting_permission") {
|
|
220
|
+
this._pendingPermissionCallId = null;
|
|
221
|
+
}
|
|
222
|
+
this.handleTranscriptEvent(event);
|
|
223
|
+
});
|
|
224
|
+
};
|
|
225
|
+
const register = (command) => {
|
|
226
|
+
registry.register(command);
|
|
227
|
+
};
|
|
228
|
+
for (const cmd of (0, ui_1.createBuiltinCommands)()) {
|
|
229
|
+
register(cmd);
|
|
230
|
+
}
|
|
231
|
+
register({
|
|
232
|
+
name: "title",
|
|
233
|
+
description: "Set the current session title",
|
|
234
|
+
type: "local",
|
|
235
|
+
async execute(ctx) {
|
|
236
|
+
const next = ctx.args.trim();
|
|
237
|
+
if (next.length === 0) {
|
|
238
|
+
ctx.output.writeError("Usage: /title <text>");
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
sessionTitle = next;
|
|
242
|
+
ctx.output.writeLine(`Title: ${next}`);
|
|
243
|
+
return undefined;
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
register({
|
|
247
|
+
name: "help",
|
|
248
|
+
description: "Show available commands",
|
|
249
|
+
type: "local",
|
|
250
|
+
async execute(ctx) {
|
|
251
|
+
const query = ctx.args.trim().replace(/^\/+/, "");
|
|
252
|
+
const all = registry.listAll().slice();
|
|
253
|
+
if (query.length > 0) {
|
|
254
|
+
const cmd = all.find((c) => c.name === query) ?? null;
|
|
255
|
+
if (!cmd) {
|
|
256
|
+
ctx.output.writeError(`Unknown command: /${query}`);
|
|
257
|
+
ctx.output.writeLine("Type /help to see all commands.");
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
ctx.output.writeLine(`/${cmd.name}`);
|
|
261
|
+
ctx.output.writeLine(` ${cmd.description}`);
|
|
262
|
+
ctx.output.writeLine(` Type: ${cmd.type}`);
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
all.sort((a, b) => a.name.localeCompare(b.name));
|
|
266
|
+
const local = all.filter((c) => c.type === "local");
|
|
267
|
+
const prompt = all.filter((c) => c.type === "prompt");
|
|
268
|
+
const ui = all.filter((c) => c.type === "ui");
|
|
269
|
+
ctx.output.writeLine("Commands:");
|
|
270
|
+
const renderGroup = (label, items) => {
|
|
271
|
+
if (items.length === 0)
|
|
272
|
+
return;
|
|
273
|
+
ctx.output.writeLine(`\n${label} (${items.length}):`);
|
|
274
|
+
for (const cmd of items) {
|
|
275
|
+
ctx.output.writeLine(` /${cmd.name} — ${cmd.description}`);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
renderGroup("Local", local);
|
|
279
|
+
renderGroup("Prompt", prompt);
|
|
280
|
+
renderGroup("UI", ui);
|
|
281
|
+
ctx.output.writeLine("\nTips:");
|
|
282
|
+
ctx.output.writeLine(" /help <name> Show details for a command");
|
|
283
|
+
ctx.output.writeLine(" Ctrl+C Abort the current turn (or exit if idle)");
|
|
284
|
+
return undefined;
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
register({
|
|
288
|
+
name: "exit",
|
|
289
|
+
description: "Exit the interactive session",
|
|
290
|
+
type: "local",
|
|
291
|
+
async execute(ctx) {
|
|
292
|
+
exitRequested = true;
|
|
293
|
+
ctx.output.writeLine("Bye.");
|
|
294
|
+
inputLoop?.close();
|
|
295
|
+
return undefined;
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
register({
|
|
299
|
+
name: "quit",
|
|
300
|
+
description: "Exit the interactive session (alias of /exit)",
|
|
301
|
+
type: "local",
|
|
302
|
+
async execute(ctx) {
|
|
303
|
+
exitRequested = true;
|
|
304
|
+
ctx.output.writeLine("Bye.");
|
|
305
|
+
inputLoop?.close();
|
|
306
|
+
return undefined;
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
register({
|
|
310
|
+
name: "clear",
|
|
311
|
+
description: "Clear the transcript",
|
|
312
|
+
type: "local",
|
|
313
|
+
async execute(ctx) {
|
|
314
|
+
ctx.output.writeLine("Transcript stays in terminal scrollback. Use your terminal clear command if you want a clean screen.");
|
|
315
|
+
return undefined;
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
register({
|
|
319
|
+
name: "sessions",
|
|
320
|
+
description: "List recent sessions",
|
|
321
|
+
type: "local",
|
|
322
|
+
async execute(ctx) {
|
|
323
|
+
const dir = (0, session_store_js_1.getSessionStoreDir)(resolveAgentDir());
|
|
324
|
+
const recent = await (0, session_store_js_1.readRecentSessions)(dir);
|
|
325
|
+
if (recent.length === 0) {
|
|
326
|
+
ctx.output.writeLine("No recent sessions.");
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
const formatAge = (ts) => {
|
|
330
|
+
const delta = Math.max(0, Date.now() - ts);
|
|
331
|
+
const seconds = Math.floor(delta / 1000);
|
|
332
|
+
if (seconds < 60)
|
|
333
|
+
return `${seconds}s ago`;
|
|
334
|
+
const minutes = Math.floor(seconds / 60);
|
|
335
|
+
if (minutes < 60)
|
|
336
|
+
return `${minutes}m ago`;
|
|
337
|
+
const hours = Math.floor(minutes / 60);
|
|
338
|
+
if (hours < 24)
|
|
339
|
+
return `${hours}h ago`;
|
|
340
|
+
const days = Math.floor(hours / 24);
|
|
341
|
+
return `${days}d ago`;
|
|
342
|
+
};
|
|
343
|
+
ctx.output.writeLine("Recent sessions:");
|
|
344
|
+
let i = 0;
|
|
345
|
+
for (const entry of recent) {
|
|
346
|
+
i++;
|
|
347
|
+
const id = entry.recoveryData.sessionId.value;
|
|
348
|
+
const model = entry.recoveryData.model.id;
|
|
349
|
+
const title = entry.title ? ` — ${entry.title}` : "";
|
|
350
|
+
const age = formatAge(entry.updatedAt);
|
|
351
|
+
ctx.output.writeLine(` #${i} ${id} (${model})${title} — ${age}`);
|
|
352
|
+
}
|
|
353
|
+
ctx.output.writeLine("Next: /resume <sessionId|#N|title> or /resume (last)");
|
|
354
|
+
return undefined;
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
register({
|
|
358
|
+
name: "changes",
|
|
359
|
+
description: "Show changed files and diff summary",
|
|
360
|
+
type: "local",
|
|
361
|
+
async execute(ctx) {
|
|
362
|
+
const cwd = ctx.session.context.workingDirectory;
|
|
363
|
+
ctx.output.writeLine("Working tree:");
|
|
364
|
+
if (handler._changedPaths.size > 0) {
|
|
365
|
+
ctx.output.writeLine("Changed files (observed by tools):");
|
|
366
|
+
for (const path of [...handler._changedPaths].sort((a, b) => a.localeCompare(b))) {
|
|
367
|
+
ctx.output.writeLine(` ${path}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
ctx.output.writeLine("Changed files (observed by tools): none");
|
|
372
|
+
}
|
|
373
|
+
const status = await runGit(cwd, ["status", "--porcelain"]);
|
|
374
|
+
if (status.type === "ok") {
|
|
375
|
+
const trimmed = status.stdout.trim();
|
|
376
|
+
ctx.output.writeLine("git status --porcelain:");
|
|
377
|
+
ctx.output.writeLine(trimmed.length > 0 ? trimmed : " clean");
|
|
378
|
+
}
|
|
379
|
+
const stat = await runGit(cwd, ["diff", "--stat"]);
|
|
380
|
+
if (stat.type === "ok" && stat.stdout.trim().length > 0) {
|
|
381
|
+
ctx.output.writeLine("git diff --stat:");
|
|
382
|
+
ctx.output.writeLine(` ${stat.stdout.trimEnd().split("\n").join("\n ")}`);
|
|
383
|
+
}
|
|
384
|
+
if (status.type === "error" || stat.type === "error") {
|
|
385
|
+
ctx.output.writeError("git not available in this working directory.");
|
|
386
|
+
ctx.output.writeLine("Next: use /review to inspect tool-observed changes.");
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
ctx.output.writeLine("Next: /review to inspect, /diff [file] to see patches, /revert <file> to undo.");
|
|
390
|
+
return undefined;
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
register({
|
|
394
|
+
name: "diff",
|
|
395
|
+
description: "Show git diff (optionally for a file)",
|
|
396
|
+
type: "local",
|
|
397
|
+
async execute(ctx) {
|
|
398
|
+
const cwd = ctx.session.context.workingDirectory;
|
|
399
|
+
const target = ctx.args.trim();
|
|
400
|
+
if (target.length === 0) {
|
|
401
|
+
ctx.output.writeLine("Diff:");
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
ctx.output.writeLine(`Diff: ${target}`);
|
|
405
|
+
}
|
|
406
|
+
const args = target.length > 0 ? ["diff", "--", target] : ["diff"];
|
|
407
|
+
const diff = await runGit(cwd, args);
|
|
408
|
+
if (diff.type === "error") {
|
|
409
|
+
ctx.output.writeError("git not available in this working directory.");
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
ctx.output.writeLine(diff.stdout.trimEnd().length > 0 ? diff.stdout.trimEnd() : "(no diff)");
|
|
413
|
+
ctx.output.writeLine("Next: /revert <file> to undo, or /review to see a summary.");
|
|
414
|
+
return undefined;
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
register({
|
|
418
|
+
name: "revert",
|
|
419
|
+
description: "Revert a file using git checkout -- <file> (or --all)",
|
|
420
|
+
type: "local",
|
|
421
|
+
async execute(ctx) {
|
|
422
|
+
const cwd = ctx.session.context.workingDirectory;
|
|
423
|
+
const arg = ctx.args.trim();
|
|
424
|
+
if (arg.length === 0) {
|
|
425
|
+
ctx.output.writeError("Usage: /revert <file> | /revert --all");
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
428
|
+
if (arg === "--all") {
|
|
429
|
+
const result = await runGit(cwd, ["checkout", "--", "."]);
|
|
430
|
+
if (result.type === "error") {
|
|
431
|
+
ctx.output.writeError("git not available in this working directory.");
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
434
|
+
handler._changedPaths.clear();
|
|
435
|
+
ctx.output.writeLine("Reverted all changes.");
|
|
436
|
+
ctx.output.writeLine("Next: /changes to confirm clean state.");
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
439
|
+
const result = await runGit(cwd, ["checkout", "--", arg]);
|
|
440
|
+
if (result.type === "error") {
|
|
441
|
+
ctx.output.writeError("git not available in this working directory.");
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
444
|
+
handler._changedPaths.delete(arg);
|
|
445
|
+
ctx.output.writeLine(`Reverted: ${arg}`);
|
|
446
|
+
ctx.output.writeLine("Next: /changes to confirm, or keep iterating.");
|
|
447
|
+
return undefined;
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
register({
|
|
451
|
+
name: "review",
|
|
452
|
+
description: "Review changes and decide to keep or revert",
|
|
453
|
+
type: "local",
|
|
454
|
+
async execute(ctx) {
|
|
455
|
+
const cwd = ctx.session.context.workingDirectory;
|
|
456
|
+
const status = await runGit(cwd, ["status", "--porcelain"]);
|
|
457
|
+
if (status.type === "ok" && status.stdout.trim().length === 0 && handler._changedPaths.size === 0) {
|
|
458
|
+
ctx.output.writeLine("Review: clean working tree.");
|
|
459
|
+
ctx.output.writeLine("Next: continue chatting, or run /status if you want a snapshot.");
|
|
460
|
+
return undefined;
|
|
461
|
+
}
|
|
462
|
+
await registry.get("changes").execute?.(ctx);
|
|
463
|
+
ctx.output.writeLine("Review tips:");
|
|
464
|
+
ctx.output.writeLine(" /diff <file> Inspect a specific patch");
|
|
465
|
+
ctx.output.writeLine(" /revert <file> Undo a change");
|
|
466
|
+
ctx.output.writeLine(" /revert --all Undo all changes");
|
|
467
|
+
ctx.output.writeLine("Next: inspect diffs, then continue chatting.");
|
|
468
|
+
return undefined;
|
|
469
|
+
},
|
|
470
|
+
});
|
|
471
|
+
register({
|
|
472
|
+
name: "status",
|
|
473
|
+
description: "Show status",
|
|
474
|
+
type: "local",
|
|
475
|
+
async execute(ctx) {
|
|
476
|
+
const state = ctx.session.state;
|
|
477
|
+
ctx.output.writeLine(`Session: ${state.id.value}`);
|
|
478
|
+
ctx.output.writeLine(` CWD: ${ctx.session.context.workingDirectory}`);
|
|
479
|
+
ctx.output.writeLine(` Agent dir: ${resolveAgentDir()}`);
|
|
480
|
+
ctx.output.writeLine(` Model: ${state.model.displayName ?? state.model.id}`);
|
|
481
|
+
ctx.output.writeLine(` Provider: ${state.model.provider}`);
|
|
482
|
+
ctx.output.writeLine(` Phase: ${interactionState.phase}`);
|
|
483
|
+
ctx.output.writeLine(` Task: ${state.taskState.status}${state.taskState.currentTaskId ? ` (${state.taskState.currentTaskId})` : ""}`);
|
|
484
|
+
ctx.output.writeLine(` Tools: ${[...state.toolSet].join(", ") || "(none)"}`);
|
|
485
|
+
if (state.planSummary) {
|
|
486
|
+
ctx.output.writeLine(` Plan: ${state.planSummary.completedSteps}/${state.planSummary.stepCount}`);
|
|
487
|
+
}
|
|
488
|
+
if (state.compactionSummary) {
|
|
489
|
+
ctx.output.writeLine(` Last compaction: ${state.compactionSummary.estimatedTokensSaved} tokens saved`);
|
|
490
|
+
}
|
|
491
|
+
if (handler._lastError) {
|
|
492
|
+
ctx.output.writeLine(` Last error: ${handler._lastError}`);
|
|
493
|
+
}
|
|
494
|
+
if (handler._changedPaths.size > 0) {
|
|
495
|
+
ctx.output.writeLine(` Changed files: ${handler._changedPaths.size}`);
|
|
496
|
+
}
|
|
497
|
+
if (handler._pendingPermissionCallId) {
|
|
498
|
+
ctx.output.writeLine(` Waiting permission: ${handler._pendingPermissionCallId}`);
|
|
499
|
+
}
|
|
500
|
+
ctx.output.writeLine("Next:");
|
|
501
|
+
if (handler._pendingPermissionCallId) {
|
|
502
|
+
ctx.output.writeLine(" Reply y (once), Y (session), n (deny), or Ctrl+C to deny");
|
|
503
|
+
}
|
|
504
|
+
else if (handler._activeTurn) {
|
|
505
|
+
ctx.output.writeLine(" Wait for the active turn, or Ctrl+C to abort");
|
|
506
|
+
}
|
|
507
|
+
else if (handler._changedPaths.size > 0) {
|
|
508
|
+
ctx.output.writeLine(" /review to inspect changes, or /diff <file>");
|
|
509
|
+
}
|
|
510
|
+
else if (handler._lastError) {
|
|
511
|
+
ctx.output.writeLine(" /doctor to diagnose, or /help for commands");
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
ctx.output.writeLine(" Type a prompt, or /help for commands");
|
|
515
|
+
}
|
|
516
|
+
return undefined;
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
register({
|
|
520
|
+
name: "usage",
|
|
521
|
+
description: "Show tool usage and governance summary",
|
|
522
|
+
type: "local",
|
|
523
|
+
async execute(ctx) {
|
|
524
|
+
const entries = ctx.runtime.governor.audit.getAll();
|
|
525
|
+
const total = entries.length;
|
|
526
|
+
const success = entries.filter((e) => e.status === "success").length;
|
|
527
|
+
const failure = entries.filter((e) => e.status === "failure").length;
|
|
528
|
+
const denied = entries.filter((e) => e.status === "denied").length;
|
|
529
|
+
ctx.output.writeLine(`Tools: ${total} total — ${success} success, ${failure} failure, ${denied} denied`);
|
|
530
|
+
const tail = entries.slice(-10);
|
|
531
|
+
if (tail.length > 0) {
|
|
532
|
+
ctx.output.writeLine("Recent:");
|
|
533
|
+
for (const entry of tail) {
|
|
534
|
+
const path = entry.targetPath ? ` ${entry.targetPath}` : "";
|
|
535
|
+
ctx.output.writeLine(` ${entry.status} ${entry.toolName} (${entry.riskLevel})${path} ${entry.durationMs}ms`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return undefined;
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
register({
|
|
542
|
+
name: "config",
|
|
543
|
+
description: "Show effective config",
|
|
544
|
+
type: "local",
|
|
545
|
+
async execute(ctx) {
|
|
546
|
+
const sources = ctx.session.context.configSources ?? {};
|
|
547
|
+
ctx.output.writeLine("Precedence: default < agent < project < env < cli");
|
|
548
|
+
const keys = Object.keys(sources).sort((a, b) => a.localeCompare(b));
|
|
549
|
+
if (keys.length > 0) {
|
|
550
|
+
ctx.output.writeLine("Sources:");
|
|
551
|
+
for (const key of keys) {
|
|
552
|
+
const source = sources[key];
|
|
553
|
+
ctx.output.writeLine(` ${key}: ${source.layer} (${source.detail})`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const agentDir = resolveAgentDir();
|
|
557
|
+
const modelsPath = (0, node_path_1.join)(agentDir, "models.json");
|
|
558
|
+
ctx.output.writeLine(`agentDir: ${agentDir}`);
|
|
559
|
+
ctx.output.writeLine(`models.json: ${modelsPath}`);
|
|
560
|
+
let raw = "";
|
|
561
|
+
try {
|
|
562
|
+
raw = await (0, promises_1.readFile)(modelsPath, "utf8");
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
ctx.output.writeError("models.json not found. Run Genesis once or pass --agent-dir.");
|
|
566
|
+
return undefined;
|
|
567
|
+
}
|
|
568
|
+
const parsed = JSON.parse(raw);
|
|
569
|
+
const providerKey = ctx.session.state.model.provider;
|
|
570
|
+
const provider = parsed.providers?.[providerKey];
|
|
571
|
+
if (!provider) {
|
|
572
|
+
ctx.output.writeError(`Provider not configured: ${providerKey}`);
|
|
573
|
+
return undefined;
|
|
574
|
+
}
|
|
575
|
+
ctx.output.writeLine(`provider: ${providerKey}`);
|
|
576
|
+
ctx.output.writeLine(` api: ${provider.api ?? "(missing)"}`);
|
|
577
|
+
ctx.output.writeLine(` baseUrl: ${provider.baseUrl ?? "(missing)"}`);
|
|
578
|
+
const apiKeyEnv = typeof provider.apiKey === "string" ? provider.apiKey : "GENESIS_API_KEY";
|
|
579
|
+
ctx.output.writeLine(` apiKey env: ${apiKeyEnv} (${process.env[apiKeyEnv] ? "set" : "missing"})`);
|
|
580
|
+
const models = Array.isArray(provider.models) ? provider.models : [];
|
|
581
|
+
const active = models.find((m) => m?.id === ctx.session.state.model.id);
|
|
582
|
+
if (active) {
|
|
583
|
+
ctx.output.writeLine(`model: ${active.name ?? active.id}`);
|
|
584
|
+
ctx.output.writeLine(` id: ${active.id}`);
|
|
585
|
+
ctx.output.writeLine(` reasoning: ${Boolean(active.reasoning)}`);
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
ctx.output.writeError(`Model not configured: ${ctx.session.state.model.id}`);
|
|
589
|
+
}
|
|
590
|
+
return undefined;
|
|
591
|
+
},
|
|
592
|
+
});
|
|
593
|
+
register({
|
|
594
|
+
name: "doctor",
|
|
595
|
+
description: "Diagnose OpenAI-compatible mainline",
|
|
596
|
+
type: "local",
|
|
597
|
+
async execute(ctx) {
|
|
598
|
+
const agentDir = resolveAgentDir();
|
|
599
|
+
const modelsPath = (0, node_path_1.join)(agentDir, "models.json");
|
|
600
|
+
let raw = "";
|
|
601
|
+
try {
|
|
602
|
+
raw = await (0, promises_1.readFile)(modelsPath, "utf8");
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
ctx.output.writeError("models.json not found.");
|
|
606
|
+
return undefined;
|
|
607
|
+
}
|
|
608
|
+
const parsed = JSON.parse(raw);
|
|
609
|
+
const providerKey = ctx.session.state.model.provider;
|
|
610
|
+
const provider = parsed.providers?.[providerKey];
|
|
611
|
+
const baseUrl = typeof provider?.baseUrl === "string" ? provider.baseUrl : "";
|
|
612
|
+
const api = typeof provider?.api === "string" ? provider.api : "";
|
|
613
|
+
const apiKeyEnv = typeof provider?.apiKey === "string" ? provider.apiKey : "GENESIS_API_KEY";
|
|
614
|
+
const apiKey = process.env[apiKeyEnv];
|
|
615
|
+
ctx.output.writeLine(`provider: ${providerKey}`);
|
|
616
|
+
ctx.output.writeLine(` api: ${api || "(missing)"}`);
|
|
617
|
+
ctx.output.writeLine(` baseUrl: ${baseUrl || "(missing)"}`);
|
|
618
|
+
ctx.output.writeLine(` apiKey env: ${apiKeyEnv} (${apiKey ? "set" : "missing"})`);
|
|
619
|
+
if (!apiKey || !baseUrl || api !== "openai-completions") {
|
|
620
|
+
return undefined;
|
|
621
|
+
}
|
|
622
|
+
const controller = new AbortController();
|
|
623
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
624
|
+
try {
|
|
625
|
+
const response = await fetch(new URL("chat/completions", baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`), {
|
|
626
|
+
method: "POST",
|
|
627
|
+
headers: {
|
|
628
|
+
"content-type": "application/json",
|
|
629
|
+
authorization: `Bearer ${apiKey}`,
|
|
630
|
+
},
|
|
631
|
+
body: JSON.stringify({
|
|
632
|
+
model: ctx.session.state.model.id,
|
|
633
|
+
stream: false,
|
|
634
|
+
messages: [{ role: "user", content: "Reply exactly DOCTOR_OK" }],
|
|
635
|
+
}),
|
|
636
|
+
signal: controller.signal,
|
|
637
|
+
});
|
|
638
|
+
ctx.output.writeLine(` http: ${response.status}`);
|
|
639
|
+
if (!response.ok) {
|
|
640
|
+
ctx.output.writeError(await response.text());
|
|
641
|
+
return undefined;
|
|
642
|
+
}
|
|
643
|
+
const payload = (await response.json());
|
|
644
|
+
const text = payload?.choices?.[0]?.message?.content;
|
|
645
|
+
if (typeof text === "string") {
|
|
646
|
+
ctx.output.writeLine(` response: ${text.trim()}`);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
catch (err) {
|
|
650
|
+
ctx.output.writeError(` error: ${err instanceof Error ? err.message : String(err)}`);
|
|
651
|
+
}
|
|
652
|
+
finally {
|
|
653
|
+
clearTimeout(timeout);
|
|
654
|
+
}
|
|
655
|
+
return undefined;
|
|
656
|
+
},
|
|
657
|
+
});
|
|
658
|
+
register({
|
|
659
|
+
name: "resume",
|
|
660
|
+
description: "Resume the last session",
|
|
661
|
+
type: "local",
|
|
662
|
+
async execute(ctx) {
|
|
663
|
+
if (handler._activeTurn || handler._pendingPermissionCallId) {
|
|
664
|
+
ctx.output.writeError("Session is busy.");
|
|
665
|
+
return undefined;
|
|
666
|
+
}
|
|
667
|
+
const dir = (0, session_store_js_1.getSessionStoreDir)(resolveAgentDir());
|
|
668
|
+
const selector = ctx.args.trim();
|
|
669
|
+
const recent = selector.length === 0 ? null : await (0, session_store_js_1.readRecentSessions)(dir);
|
|
670
|
+
const data = selector.length === 0
|
|
671
|
+
? await (0, session_store_js_1.readLastSession)(dir)
|
|
672
|
+
: (() => {
|
|
673
|
+
if (!recent)
|
|
674
|
+
return null;
|
|
675
|
+
const idxText = selector.startsWith("#") ? selector.slice(1) : selector;
|
|
676
|
+
const idx = Number.parseInt(idxText, 10);
|
|
677
|
+
if (Number.isFinite(idx) && idx >= 1 && idx <= recent.length) {
|
|
678
|
+
return recent[idx - 1]?.recoveryData ?? null;
|
|
679
|
+
}
|
|
680
|
+
const exact = recent.find((entry) => entry.recoveryData.sessionId.value === selector)?.recoveryData ??
|
|
681
|
+
null;
|
|
682
|
+
if (exact)
|
|
683
|
+
return exact;
|
|
684
|
+
const prefixMatches = recent.filter((entry) => entry.recoveryData.sessionId.value.startsWith(selector));
|
|
685
|
+
if (prefixMatches.length === 1)
|
|
686
|
+
return prefixMatches[0].recoveryData;
|
|
687
|
+
const q = selector.toLowerCase();
|
|
688
|
+
const titleMatches = recent.filter((entry) => (entry.title ?? "").toLowerCase().includes(q));
|
|
689
|
+
if (titleMatches.length === 1)
|
|
690
|
+
return titleMatches[0].recoveryData;
|
|
691
|
+
const candidates = [...prefixMatches, ...titleMatches].slice(0, 10);
|
|
692
|
+
if (candidates.length > 1) {
|
|
693
|
+
ctx.output.writeLine("Multiple matches:");
|
|
694
|
+
let i = 0;
|
|
695
|
+
for (const entry of candidates) {
|
|
696
|
+
i++;
|
|
697
|
+
const id = entry.recoveryData.sessionId.value;
|
|
698
|
+
const model = entry.recoveryData.model.id;
|
|
699
|
+
const title = entry.title ? ` — ${entry.title}` : "";
|
|
700
|
+
ctx.output.writeLine(` #${i} ${id} (${model})${title}`);
|
|
701
|
+
}
|
|
702
|
+
ctx.output.writeLine("Tip: use an exact sessionId, or /sessions then /resume #N.");
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
return null;
|
|
706
|
+
})();
|
|
707
|
+
if (!data) {
|
|
708
|
+
ctx.output.writeError(selector.length === 0 ? "No previous session found." : `Session not found: ${selector}`);
|
|
709
|
+
return undefined;
|
|
710
|
+
}
|
|
711
|
+
handler._suppressPersistOnce = true;
|
|
712
|
+
await sessionRef.current.close();
|
|
713
|
+
const recovered = runtime.recoverSession(data);
|
|
714
|
+
attachSession(recovered);
|
|
715
|
+
ctx.output.writeLine(`Resumed: ${data.sessionId.value}`);
|
|
716
|
+
handler.renderPromptLine();
|
|
717
|
+
return undefined;
|
|
718
|
+
},
|
|
719
|
+
});
|
|
720
|
+
attachSession(sessionRef.current);
|
|
721
|
+
// Input loop
|
|
722
|
+
inputLoop = (0, input_loop_js_1.createInputLoop)({
|
|
723
|
+
prompt: "",
|
|
724
|
+
rawMode: true,
|
|
725
|
+
submitNewline: false,
|
|
726
|
+
onInputStateChange: (state) => {
|
|
727
|
+
this._inputState = state;
|
|
728
|
+
this._commandSuggestions = computeSlashSuggestions(state.buffer, registry.listAll());
|
|
729
|
+
this.renderPromptLine();
|
|
730
|
+
},
|
|
731
|
+
onTabComplete: (state) => {
|
|
732
|
+
if (this._pendingPermissionCallId !== null) {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
const nextState = acceptFirstSlashSuggestion(state, this._commandSuggestions);
|
|
736
|
+
if (nextState) {
|
|
737
|
+
this._inputState = nextState;
|
|
738
|
+
this._commandSuggestions = computeSlashSuggestions(nextState.buffer, registry.listAll());
|
|
739
|
+
this.renderPromptLine();
|
|
740
|
+
}
|
|
741
|
+
return nextState;
|
|
742
|
+
},
|
|
743
|
+
onKey: (key) => {
|
|
744
|
+
if (key === "ctrlc") {
|
|
745
|
+
if (this._pendingPermissionCallId !== null) {
|
|
746
|
+
const callId = this._pendingPermissionCallId;
|
|
747
|
+
this._pendingPermissionCallId = null;
|
|
748
|
+
this._pendingPermissionDetails = null;
|
|
749
|
+
void sessionRef.current.resolvePermission(callId, "deny").catch((err) => {
|
|
750
|
+
sink.writeError(`Error: ${err}`);
|
|
751
|
+
});
|
|
752
|
+
sink.writeLine("Permission denied.");
|
|
753
|
+
this.renderPromptLine();
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
if (this._activeTurn !== null) {
|
|
757
|
+
sessionRef.current.abort();
|
|
758
|
+
this.flushAssistantBuffer(false);
|
|
759
|
+
sink.writeLine("Aborted.");
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
exitRequested = true;
|
|
763
|
+
sink.writeLine("Bye.");
|
|
764
|
+
inputLoop?.close();
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
this.handleSpecialKey(key);
|
|
768
|
+
},
|
|
769
|
+
onTerminalEvent: (event) => {
|
|
770
|
+
if (event === "focusin") {
|
|
771
|
+
ttySession.refresh();
|
|
772
|
+
this.rerenderInteractiveRegions();
|
|
773
|
+
}
|
|
774
|
+
},
|
|
775
|
+
});
|
|
776
|
+
ttySession.enter();
|
|
777
|
+
this.renderWelcome(sessionRef.current);
|
|
778
|
+
this.fullRedrawInteractiveScreen();
|
|
779
|
+
try {
|
|
780
|
+
let line = await inputLoop.nextLine();
|
|
781
|
+
while (line !== null) {
|
|
782
|
+
const trimmed = line.trim();
|
|
783
|
+
// Permission response
|
|
784
|
+
if (this._pendingPermissionCallId !== null) {
|
|
785
|
+
const decision = parsePermissionDecision(trimmed, this._pendingPermissionSelection);
|
|
786
|
+
if (!decision) {
|
|
787
|
+
sink.writeError("Permission: use 1/2/3, Enter, y/Y/n, or arrow keys/Tab to choose.");
|
|
788
|
+
line = await inputLoop.nextLine();
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
await sessionRef.current.resolvePermission(this._pendingPermissionCallId, decision);
|
|
792
|
+
this._pendingPermissionCallId = null;
|
|
793
|
+
this._pendingPermissionDetails = null;
|
|
794
|
+
this._pendingPermissionSelection = 0;
|
|
795
|
+
line = await inputLoop.nextLine();
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
if (trimmed.length === 0) {
|
|
799
|
+
line = await inputLoop.nextLine();
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
// Check for slash commands
|
|
803
|
+
const resolution = registry.resolve(trimmed);
|
|
804
|
+
if (resolution && resolution.type === "command") {
|
|
805
|
+
await resolution.command.execute?.({
|
|
806
|
+
args: resolution.args,
|
|
807
|
+
runtime,
|
|
808
|
+
session: sessionRef.current,
|
|
809
|
+
output: sink,
|
|
810
|
+
});
|
|
811
|
+
if (exitRequested) {
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
line = await inputLoop.nextLine();
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
if (resolution && resolution.type === "not_found") {
|
|
818
|
+
sink.writeError(`Unknown command: /${resolution.name}. Type /help for a list.`);
|
|
819
|
+
line = await inputLoop.nextLine();
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
// Regular prompt
|
|
823
|
+
if (this._activeTurn !== null) {
|
|
824
|
+
this._queuedInputs.push(trimmed);
|
|
825
|
+
line = await inputLoop.nextLine();
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
this.startPromptTurn(sessionRef.current, trimmed, sink);
|
|
829
|
+
line = await inputLoop.nextLine();
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
finally {
|
|
833
|
+
process.stdout.off("resize", onResize);
|
|
834
|
+
inputLoop.close();
|
|
835
|
+
process.stdout.write(ansiResetScrollRegion());
|
|
836
|
+
ttySession.restore();
|
|
837
|
+
sessionRef.current.events.removeAllListeners();
|
|
838
|
+
await sessionRef.current.close();
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
renderWelcome(session) {
|
|
842
|
+
this._welcomeLines = buildWelcomeLines({
|
|
843
|
+
terminalWidth: process.stdout.columns ?? 80,
|
|
844
|
+
version: process.env.npm_package_version ?? "dev",
|
|
845
|
+
model: session.state.model.displayName ?? session.state.model.id,
|
|
846
|
+
provider: session.state.model.provider,
|
|
847
|
+
greeting: pickWelcomeGreeting(),
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
renderPromptLine() {
|
|
851
|
+
this.renderFooterRegion();
|
|
852
|
+
}
|
|
853
|
+
handleSpecialKey(key) {
|
|
854
|
+
if (this._pendingPermissionCallId !== null) {
|
|
855
|
+
if (key === "up" || key === "shifttab") {
|
|
856
|
+
this._pendingPermissionSelection = movePermissionSelection(this._pendingPermissionSelection, -1);
|
|
857
|
+
this.renderPermissionUi();
|
|
858
|
+
}
|
|
859
|
+
else if (key === "down" || key === "tab") {
|
|
860
|
+
this._pendingPermissionSelection = movePermissionSelection(this._pendingPermissionSelection, 1);
|
|
861
|
+
this.renderPermissionUi();
|
|
862
|
+
}
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
if (key === "up" || key === "down") {
|
|
866
|
+
this.navigateHistory(key === "up" ? -1 : 1);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
renderPermissionUi() {
|
|
870
|
+
if (this._pendingPermissionCallId === null || this._pendingPermissionDetails === null) {
|
|
871
|
+
this.fullRedrawInteractiveScreen();
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
this.fullRedrawInteractiveScreen();
|
|
875
|
+
}
|
|
876
|
+
handleTranscriptEvent(event) {
|
|
877
|
+
if (event.category === "permission") {
|
|
878
|
+
if (event.type === "permission_requested") {
|
|
879
|
+
this.renderPermissionUi();
|
|
880
|
+
}
|
|
881
|
+
else {
|
|
882
|
+
this.fullRedrawInteractiveScreen();
|
|
883
|
+
}
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
if (event.category === "tool") {
|
|
887
|
+
const text = formatInteractiveToolEvent(event, this._toolCalls.get(event.toolCallId)?.parameters);
|
|
888
|
+
if (text.length > 0) {
|
|
889
|
+
this.flushAssistantBuffer(false);
|
|
890
|
+
this.writeTranscriptText(text, true);
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
this.renderPromptLine();
|
|
894
|
+
}
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
if (!shouldRenderInteractiveTranscriptEvent(event)) {
|
|
898
|
+
this.renderPromptLine();
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
if (event.category === "text" && event.type === "thinking_delta") {
|
|
902
|
+
if (this._turnNotice === null) {
|
|
903
|
+
this.startTurnFeedback();
|
|
904
|
+
}
|
|
905
|
+
this.renderPromptLine();
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
if (event.category === "text" && event.type === "text_delta") {
|
|
909
|
+
if (this._turnNotice !== "responding") {
|
|
910
|
+
this._turnNotice = "responding";
|
|
911
|
+
}
|
|
912
|
+
this._assistantBuffer = mergeStreamingText(this._assistantBuffer, event.content);
|
|
913
|
+
this.renderStreamingAssistantBlock();
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
this.flushAssistantBuffer(false);
|
|
917
|
+
const text = (0, ui_1.formatEventAsText)(event);
|
|
918
|
+
if (text.length > 0) {
|
|
919
|
+
this.writeTranscriptText(text, true);
|
|
920
|
+
}
|
|
921
|
+
else {
|
|
922
|
+
this.renderPromptLine();
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
flushAssistantBuffer(redrawPrompt) {
|
|
926
|
+
if (this._assistantBuffer.length === 0) {
|
|
927
|
+
if (redrawPrompt) {
|
|
928
|
+
this.renderPromptLine();
|
|
929
|
+
}
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
const assistantBlock = materializeAssistantTranscriptBlock(this._assistantBuffer);
|
|
933
|
+
if (assistantBlock !== null) {
|
|
934
|
+
this.rememberAssistantTranscriptBlock(assistantBlock);
|
|
935
|
+
}
|
|
936
|
+
this._assistantBuffer = "";
|
|
937
|
+
this._streamingReservedRows = 0;
|
|
938
|
+
this._streamingDisplayRows = 0;
|
|
939
|
+
this._renderedStreamingStartRow = null;
|
|
940
|
+
if (redrawPrompt) {
|
|
941
|
+
this.renderPromptLine();
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
startTurnFeedback() {
|
|
945
|
+
if (this._turnNotice !== null) {
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
this._turnNotice = "thinking";
|
|
949
|
+
this.renderPromptLine();
|
|
950
|
+
}
|
|
951
|
+
renderStreamingAssistantBlock() {
|
|
952
|
+
const rendered = formatTranscriptAssistantLine(this._assistantBuffer);
|
|
953
|
+
const lines = wrapTranscriptContent(rendered, process.stdout.columns ?? 80);
|
|
954
|
+
const renderedWidth = this.terminalWidth();
|
|
955
|
+
const rows = countRenderedTerminalRows(lines, renderedWidth);
|
|
956
|
+
const previousStartRow = this._renderedStreamingStartRow;
|
|
957
|
+
const previousRows = this._streamingReservedRows;
|
|
958
|
+
this._streamingDisplayRows = rows;
|
|
959
|
+
this.renderFooterRegion();
|
|
960
|
+
const footerStartRow = this._renderedFooterStartRow ??
|
|
961
|
+
computeFooterStartRow(this._welcomeLines.length, this.terminalHeight(), this.currentFooterHeight(), this.currentTranscriptDisplayRows());
|
|
962
|
+
if (isFooterBottomAnchored(footerStartRow, this.terminalHeight(), this.currentFooterHeight())) {
|
|
963
|
+
this.reserveStreamingRows(rows);
|
|
964
|
+
}
|
|
965
|
+
else {
|
|
966
|
+
this._streamingReservedRows = rows;
|
|
967
|
+
}
|
|
968
|
+
const startRow = footerStartRow - rows;
|
|
969
|
+
const transcriptBottomRow = footerStartRow - 1;
|
|
970
|
+
const clearStartRow = previousStartRow === null ? startRow : Math.min(startRow, previousStartRow);
|
|
971
|
+
const clearEndRow = previousStartRow === null
|
|
972
|
+
? transcriptBottomRow
|
|
973
|
+
: Math.max(transcriptBottomRow, previousStartRow + previousRows - 1);
|
|
974
|
+
this.clearTranscriptRows(clearStartRow, clearEndRow);
|
|
975
|
+
this.writeLinesAtRow(startRow, lines, renderedWidth);
|
|
976
|
+
this._renderedStreamingStartRow = startRow;
|
|
977
|
+
this.renderFooterRegion();
|
|
978
|
+
}
|
|
979
|
+
writeTranscriptText(text, newline, redrawPrompt = true) {
|
|
980
|
+
this.flushAssistantBuffer(false);
|
|
981
|
+
this.rememberTranscriptBlock(text, newline);
|
|
982
|
+
const logicalLines = text.split("\n");
|
|
983
|
+
const outputLines = newline ? logicalLines : logicalLines.slice(0, -1).concat(logicalLines.at(-1) ?? "");
|
|
984
|
+
if (outputLines.length > 0) {
|
|
985
|
+
this.appendTranscriptLines(outputLines);
|
|
986
|
+
}
|
|
987
|
+
if (redrawPrompt) {
|
|
988
|
+
this.renderFooterRegion();
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
rememberHistory(line) {
|
|
992
|
+
if (line.length === 0)
|
|
993
|
+
return;
|
|
994
|
+
if (this._history.at(-1) === line)
|
|
995
|
+
return;
|
|
996
|
+
this._history.push(line);
|
|
997
|
+
if (this._history.length > 200) {
|
|
998
|
+
this._history.shift();
|
|
999
|
+
}
|
|
1000
|
+
this._historyIndex = null;
|
|
1001
|
+
}
|
|
1002
|
+
navigateHistory(direction) {
|
|
1003
|
+
if (this._history.length === 0)
|
|
1004
|
+
return;
|
|
1005
|
+
if (this._historyIndex === null) {
|
|
1006
|
+
this._historyIndex = this._history.length;
|
|
1007
|
+
}
|
|
1008
|
+
const next = Math.max(0, Math.min(this._history.length, this._historyIndex + direction));
|
|
1009
|
+
this._historyIndex = next;
|
|
1010
|
+
const text = next === this._history.length ? "" : (this._history[next] ?? "");
|
|
1011
|
+
this._inputState = { buffer: text, cursor: text.length };
|
|
1012
|
+
this.renderPromptLine();
|
|
1013
|
+
}
|
|
1014
|
+
startPromptTurn(session, prompt, sink) {
|
|
1015
|
+
this.flushAssistantBuffer(false);
|
|
1016
|
+
this.writeTranscriptText(formatTranscriptUserLine(prompt), true, false);
|
|
1017
|
+
this.startTurnFeedback();
|
|
1018
|
+
this.rememberHistory(prompt);
|
|
1019
|
+
this._activeTurn = session
|
|
1020
|
+
.prompt(prompt)
|
|
1021
|
+
.catch((err) => {
|
|
1022
|
+
sink.writeError(`Error: ${err}`);
|
|
1023
|
+
})
|
|
1024
|
+
.finally(() => {
|
|
1025
|
+
this._activeTurn = null;
|
|
1026
|
+
this.flushAssistantBuffer(false);
|
|
1027
|
+
this._turnNotice = null;
|
|
1028
|
+
const nextQueued = this._queuedInputs.shift();
|
|
1029
|
+
if (nextQueued) {
|
|
1030
|
+
this.startPromptTurn(session, nextQueued, sink);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
this.fullRedrawInteractiveScreen();
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
buildFooterUi() {
|
|
1037
|
+
return formatInteractiveFooter({
|
|
1038
|
+
terminalWidth: process.stdout.columns ?? 80,
|
|
1039
|
+
prompt: this._prompt,
|
|
1040
|
+
buffer: this._inputState.buffer,
|
|
1041
|
+
cursor: this._inputState.cursor,
|
|
1042
|
+
suggestions: this._commandSuggestions,
|
|
1043
|
+
turnNotice: this._turnNotice,
|
|
1044
|
+
permission: this._pendingPermissionCallId !== null && this._pendingPermissionDetails !== null
|
|
1045
|
+
? {
|
|
1046
|
+
details: this._pendingPermissionDetails,
|
|
1047
|
+
selectedIndex: this._pendingPermissionSelection,
|
|
1048
|
+
}
|
|
1049
|
+
: null,
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
rerenderInteractiveRegions() {
|
|
1053
|
+
this.fullRedrawInteractiveScreen();
|
|
1054
|
+
}
|
|
1055
|
+
terminalWidth() {
|
|
1056
|
+
return Math.max(1, process.stdout.columns ?? 80);
|
|
1057
|
+
}
|
|
1058
|
+
terminalHeight() {
|
|
1059
|
+
return Math.max(6, process.stdout.rows ?? 24);
|
|
1060
|
+
}
|
|
1061
|
+
transcriptBottomRow(footerHeight = this.currentFooterHeight()) {
|
|
1062
|
+
return Math.max(1, this.terminalHeight() - footerHeight);
|
|
1063
|
+
}
|
|
1064
|
+
currentFooterHeight() {
|
|
1065
|
+
return this._renderedFooterUi?.lines.length ?? this.buildFooterUi().lines.length;
|
|
1066
|
+
}
|
|
1067
|
+
renderFooterRegion() {
|
|
1068
|
+
const ui = this.buildFooterUi();
|
|
1069
|
+
const footerHeight = ui.lines.length;
|
|
1070
|
+
const startRow = computeFooterStartRow(this._welcomeLines.length, this.terminalHeight(), footerHeight, this.currentTranscriptDisplayRows());
|
|
1071
|
+
const oldStartRow = this._renderedFooterStartRow;
|
|
1072
|
+
const oldHeight = this._renderedFooterUi?.lines.length ?? 0;
|
|
1073
|
+
if (oldStartRow !== null && oldStartRow !== startRow) {
|
|
1074
|
+
for (let index = 0; index < oldHeight; index += 1) {
|
|
1075
|
+
this.writeAbsoluteTerminalLine(oldStartRow + index, "");
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
if (startRow === Math.max(1, this.terminalHeight() - footerHeight + 1)) {
|
|
1079
|
+
this.applyTranscriptViewport(footerHeight);
|
|
1080
|
+
}
|
|
1081
|
+
for (let index = 0; index < footerHeight; index += 1) {
|
|
1082
|
+
const row = startRow + index;
|
|
1083
|
+
const line = fitTerminalLine(ui.lines[index] ?? "", this.terminalWidth());
|
|
1084
|
+
this.writeAbsoluteTerminalLine(row, line);
|
|
1085
|
+
}
|
|
1086
|
+
for (let index = footerHeight; index < oldHeight; index += 1) {
|
|
1087
|
+
this.writeAbsoluteTerminalLine(startRow + index, "");
|
|
1088
|
+
}
|
|
1089
|
+
process.stdout.write(ansiCursorTo(startRow + ui.cursorLineIndex, computeFooterCursorColumn(this.terminalWidth(), ui.cursorColumn) + 1));
|
|
1090
|
+
process.stdout.write((0, ui_1.ansiShowCursor)());
|
|
1091
|
+
this._renderedFooterUi = { ...ui, renderedWidth: this.terminalWidth() };
|
|
1092
|
+
this._renderedFooterStartRow = startRow;
|
|
1093
|
+
}
|
|
1094
|
+
applyTranscriptViewport(footerHeight) {
|
|
1095
|
+
process.stdout.write(ansiSetScrollRegion(1, this.transcriptBottomRow(footerHeight)));
|
|
1096
|
+
}
|
|
1097
|
+
appendTranscriptLines(lines) {
|
|
1098
|
+
if (lines.length === 0) {
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
this.fullRedrawInteractiveScreen();
|
|
1102
|
+
}
|
|
1103
|
+
reserveStreamingRows(rows) {
|
|
1104
|
+
if (rows <= this._streamingReservedRows) {
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
this.applyTranscriptViewport(this.currentFooterHeight());
|
|
1108
|
+
for (let index = this._streamingReservedRows; index < rows; index += 1) {
|
|
1109
|
+
process.stdout.write(ansiCursorTo(this.transcriptBottomRow(), 1));
|
|
1110
|
+
process.stdout.write("\n");
|
|
1111
|
+
}
|
|
1112
|
+
this._streamingReservedRows = rows;
|
|
1113
|
+
}
|
|
1114
|
+
clearTranscriptRows(startRow, endRow) {
|
|
1115
|
+
for (let row = startRow; row <= endRow; row += 1) {
|
|
1116
|
+
this.writeAbsoluteTerminalLine(row, "");
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
writeLinesAtRow(startRow, lines, width) {
|
|
1120
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
1121
|
+
this.writeAbsoluteTerminalLine(startRow + index, fitTerminalLine(lines[index] ?? "", width));
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
writeAbsoluteTerminalLine(row, line) {
|
|
1125
|
+
process.stdout.write(ansiDisableAutoWrap());
|
|
1126
|
+
process.stdout.write(ansiCursorTo(row, 1));
|
|
1127
|
+
process.stdout.write((0, ui_1.ansiClearLine)());
|
|
1128
|
+
process.stdout.write(line);
|
|
1129
|
+
process.stdout.write(ansiEnableAutoWrap());
|
|
1130
|
+
}
|
|
1131
|
+
rememberTranscriptBlock(text, newline) {
|
|
1132
|
+
const block = newline ? text : text.replace(/\n$/, "");
|
|
1133
|
+
if (block.length === 0) {
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
this._transcriptBlocks.push(block);
|
|
1137
|
+
}
|
|
1138
|
+
rememberAssistantTranscriptBlock(block) {
|
|
1139
|
+
const nextBlocks = appendAssistantTranscriptBlock(this._transcriptBlocks, block);
|
|
1140
|
+
this._transcriptBlocks.length = 0;
|
|
1141
|
+
this._transcriptBlocks.push(...nextBlocks);
|
|
1142
|
+
}
|
|
1143
|
+
fullRedrawInteractiveScreen() {
|
|
1144
|
+
if (this._welcomeLines.length === 0) {
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
process.stdout.write(ansiResetScrollRegion());
|
|
1148
|
+
process.stdout.write((0, ui_1.ansiCursorHome)());
|
|
1149
|
+
process.stdout.write("\x1b[2J");
|
|
1150
|
+
for (let index = 0; index < this._welcomeLines.length; index += 1) {
|
|
1151
|
+
this.writeAbsoluteTerminalLine(index + 1, fitTerminalLine(this._welcomeLines[index] ?? "", this.terminalWidth()));
|
|
1152
|
+
}
|
|
1153
|
+
this._renderedFooterUi = null;
|
|
1154
|
+
this._renderedFooterStartRow = null;
|
|
1155
|
+
this._streamingReservedRows = 0;
|
|
1156
|
+
this._renderedStreamingStartRow = null;
|
|
1157
|
+
this._streamingDisplayRows =
|
|
1158
|
+
this._assistantBuffer.length > 0
|
|
1159
|
+
? countRenderedTerminalRows(wrapTranscriptContent(formatTranscriptAssistantLine(this._assistantBuffer), this.terminalWidth()), this.terminalWidth())
|
|
1160
|
+
: 0;
|
|
1161
|
+
this.renderTranscriptViewport();
|
|
1162
|
+
this.renderFooterRegion();
|
|
1163
|
+
if (this._assistantBuffer.length > 0) {
|
|
1164
|
+
this.renderStreamingAssistantBlock();
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
renderTranscriptViewport() {
|
|
1168
|
+
const footerUi = this.buildFooterUi();
|
|
1169
|
+
const transcriptTopRow = this._welcomeLines.length + 1;
|
|
1170
|
+
const transcriptBottomRow = computeFooterStartRow(this._welcomeLines.length, this.terminalHeight(), footerUi.lines.length, this.currentTranscriptDisplayRows()) - 1;
|
|
1171
|
+
const availableRows = Math.max(0, transcriptBottomRow - transcriptTopRow + 1);
|
|
1172
|
+
const visibleLines = computeVisibleTranscriptLines(this._transcriptBlocks, this.terminalWidth(), availableRows);
|
|
1173
|
+
for (let row = transcriptTopRow; row <= transcriptBottomRow; row += 1) {
|
|
1174
|
+
const index = row - transcriptTopRow;
|
|
1175
|
+
this.writeAbsoluteTerminalLine(row, fitTerminalLine(visibleLines[index] ?? "", this.terminalWidth()));
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
currentTranscriptDisplayRows() {
|
|
1179
|
+
return computeTranscriptDisplayRows(this._transcriptBlocks, this.terminalWidth()) + this._streamingDisplayRows;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
// ---------------------------------------------------------------------------
|
|
1183
|
+
// Print mode
|
|
1184
|
+
// ---------------------------------------------------------------------------
|
|
1185
|
+
class PrintModeHandler {
|
|
1186
|
+
async start(runtime) {
|
|
1187
|
+
const session = runtime.createSession();
|
|
1188
|
+
// Subscribe to events and format as text
|
|
1189
|
+
session.events.onAny((event) => {
|
|
1190
|
+
const text = (0, ui_1.formatEventAsText)(event);
|
|
1191
|
+
if (text.length > 0) {
|
|
1192
|
+
process.stdout.write(`${text}\n`);
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
// Read one prompt from stdin, send it, wait for completion
|
|
1196
|
+
const inputLoop = (0, input_loop_js_1.createInputLoop)({ prompt: "" });
|
|
1197
|
+
try {
|
|
1198
|
+
const line = await inputLoop.nextLine();
|
|
1199
|
+
if (line && line.trim().length > 0) {
|
|
1200
|
+
await session.prompt(line.trim());
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
finally {
|
|
1204
|
+
inputLoop.close();
|
|
1205
|
+
await session.close();
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
// ---------------------------------------------------------------------------
|
|
1210
|
+
// JSON mode
|
|
1211
|
+
// ---------------------------------------------------------------------------
|
|
1212
|
+
class JsonModeHandler {
|
|
1213
|
+
async start(runtime) {
|
|
1214
|
+
const session = runtime.createSession();
|
|
1215
|
+
// Subscribe to events and emit JSON envelopes
|
|
1216
|
+
session.events.onAny((event) => {
|
|
1217
|
+
const envelope = (0, ui_1.eventToJsonEnvelope)(event);
|
|
1218
|
+
process.stdout.write(`${JSON.stringify(envelope)}\n`);
|
|
1219
|
+
});
|
|
1220
|
+
// Also forward global events
|
|
1221
|
+
runtime.events.onAny((event) => {
|
|
1222
|
+
const envelope = (0, ui_1.eventToJsonEnvelope)(event);
|
|
1223
|
+
process.stdout.write(`${JSON.stringify(envelope)}\n`);
|
|
1224
|
+
});
|
|
1225
|
+
// Read one prompt from stdin
|
|
1226
|
+
const inputLoop = (0, input_loop_js_1.createInputLoop)({ prompt: "" });
|
|
1227
|
+
try {
|
|
1228
|
+
const line = await inputLoop.nextLine();
|
|
1229
|
+
if (line && line.trim().length > 0) {
|
|
1230
|
+
await session.prompt(line.trim());
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
finally {
|
|
1234
|
+
inputLoop.close();
|
|
1235
|
+
await session.close();
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
// ---------------------------------------------------------------------------
|
|
1240
|
+
// RPC mode
|
|
1241
|
+
// ---------------------------------------------------------------------------
|
|
1242
|
+
class RpcModeHandler {
|
|
1243
|
+
server = null;
|
|
1244
|
+
async start(runtime) {
|
|
1245
|
+
this.server = (0, rpc_server_js_1.createRpcServer)();
|
|
1246
|
+
await this.server.start(runtime);
|
|
1247
|
+
}
|
|
1248
|
+
async stop() {
|
|
1249
|
+
if (this.server) {
|
|
1250
|
+
await this.server.stop();
|
|
1251
|
+
this.server = null;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
function parsePermissionDecision(input, selectedIndex = 0) {
|
|
1256
|
+
const trimmed = input.trim();
|
|
1257
|
+
if (trimmed.length === 0)
|
|
1258
|
+
return permissionDecisionFromSelection(selectedIndex);
|
|
1259
|
+
if (trimmed === "y" || trimmed.toLowerCase() === "yes")
|
|
1260
|
+
return "allow_once";
|
|
1261
|
+
if (trimmed === "Y")
|
|
1262
|
+
return "allow_for_session";
|
|
1263
|
+
if (trimmed === "n" || trimmed.toLowerCase() === "no")
|
|
1264
|
+
return "deny";
|
|
1265
|
+
if (trimmed === "1")
|
|
1266
|
+
return "allow_once";
|
|
1267
|
+
if (trimmed === "2")
|
|
1268
|
+
return "allow_for_session";
|
|
1269
|
+
if (trimmed === "3")
|
|
1270
|
+
return "deny";
|
|
1271
|
+
return null;
|
|
1272
|
+
}
|
|
1273
|
+
function runGit(cwd, args) {
|
|
1274
|
+
return new Promise((resolve) => {
|
|
1275
|
+
(0, node_child_process_1.execFile)("git", [...args], { cwd }, (error, stdout, stderr) => {
|
|
1276
|
+
if (error) {
|
|
1277
|
+
resolve({ type: "error" });
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
resolve({ type: "ok", stdout: String(stdout), stderr: String(stderr) });
|
|
1281
|
+
});
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
function stripAnsiWelcome(text) {
|
|
1285
|
+
return text.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;?]*[ -/]*[@-~]`, "g"), "");
|
|
1286
|
+
}
|
|
1287
|
+
function applyWelcomeBorderColor(text) {
|
|
1288
|
+
return `${theme_js_1.INTERACTIVE_THEME.welcomeBorder}${text}${theme_js_1.INTERACTIVE_THEME.reset}`;
|
|
1289
|
+
}
|
|
1290
|
+
function buildWelcomeHintLine(width) {
|
|
1291
|
+
if (width < 64) {
|
|
1292
|
+
return "Start: Enter Help: /help Scroll: wheel/PageUp/PageDown";
|
|
1293
|
+
}
|
|
1294
|
+
return "Start: type a prompt and press Enter Help: /help Scroll: wheel/PageUp/PageDown";
|
|
1295
|
+
}
|
|
1296
|
+
exports.WELCOME_BIBLE_GREETINGS = [
|
|
1297
|
+
"Let there be light.",
|
|
1298
|
+
"Seek, and ye shall find.",
|
|
1299
|
+
"Knock, and it shall be opened.",
|
|
1300
|
+
"Write the vision plainly.",
|
|
1301
|
+
"Iron sharpeneth iron.",
|
|
1302
|
+
"The truth shall make you free.",
|
|
1303
|
+
"A wise man will hear.",
|
|
1304
|
+
"Let all things be done decently.",
|
|
1305
|
+
];
|
|
1306
|
+
function pickWelcomeGreeting(randomValue = Math.random()) {
|
|
1307
|
+
const size = exports.WELCOME_BIBLE_GREETINGS.length;
|
|
1308
|
+
const normalized = Number.isFinite(randomValue) ? Math.min(Math.max(randomValue, 0), 0.999999999999) : 0;
|
|
1309
|
+
return exports.WELCOME_BIBLE_GREETINGS[Math.floor(normalized * size)] ?? exports.WELCOME_BIBLE_GREETINGS[0];
|
|
1310
|
+
}
|
|
1311
|
+
function buildWelcomeLines(input) {
|
|
1312
|
+
const width = Math.max(24, Math.min(input.terminalWidth, 100));
|
|
1313
|
+
const DIM = theme_js_1.INTERACTIVE_THEME.muted;
|
|
1314
|
+
const RESET = theme_js_1.INTERACTIVE_THEME.reset;
|
|
1315
|
+
const GREEN = theme_js_1.INTERACTIVE_THEME.success;
|
|
1316
|
+
const CYAN = theme_js_1.INTERACTIVE_THEME.brand;
|
|
1317
|
+
const BOLD = theme_js_1.INTERACTIVE_THEME.bold;
|
|
1318
|
+
const contentWidth = width - 2;
|
|
1319
|
+
const center = (text) => formatWelcomeCenteredLine(contentWidth, text);
|
|
1320
|
+
const fill = (text = "") => formatWelcomeFilledLine(contentWidth, text);
|
|
1321
|
+
return [
|
|
1322
|
+
formatWelcomeTopBorder(width, input.version),
|
|
1323
|
+
fill(),
|
|
1324
|
+
center(`${BOLD}${input.greeting}${RESET}`),
|
|
1325
|
+
fill(),
|
|
1326
|
+
center(`${DIM} ${GREEN}✦${RESET} ${RESET}`),
|
|
1327
|
+
center(`${CYAN} ──╂── ${RESET}`),
|
|
1328
|
+
center(`${DIM} ${CYAN}│${RESET} ${RESET}`),
|
|
1329
|
+
fill(),
|
|
1330
|
+
center(`${CYAN}${input.model}${RESET} ${DIM}via${RESET} ${input.provider}`),
|
|
1331
|
+
formatWelcomeBottomBorder(width),
|
|
1332
|
+
buildWelcomeHintLine(width),
|
|
1333
|
+
];
|
|
1334
|
+
}
|
|
1335
|
+
function formatWelcomeTopBorder(width, version) {
|
|
1336
|
+
const label = `╭─── ${theme_js_1.INTERACTIVE_THEME.bold}${theme_js_1.INTERACTIVE_THEME.welcomeTitle}Genesis CLI${theme_js_1.INTERACTIVE_THEME.reset} ${theme_js_1.INTERACTIVE_THEME.muted}v${version}${theme_js_1.INTERACTIVE_THEME.reset} `;
|
|
1337
|
+
const plainWidth = (0, terminal_display_width_js_1.measureTerminalDisplayWidth)(stripAnsiWelcome(label));
|
|
1338
|
+
return applyWelcomeBorderColor(`${label}${"─".repeat(Math.max(0, width - plainWidth - 1))}╮`);
|
|
1339
|
+
}
|
|
1340
|
+
function formatWelcomeBottomBorder(width) {
|
|
1341
|
+
return applyWelcomeBorderColor(`╰${"─".repeat(Math.max(0, width - 2))}╯`);
|
|
1342
|
+
}
|
|
1343
|
+
function formatWelcomeFilledLine(contentWidth, text = "") {
|
|
1344
|
+
const plainWidth = (0, terminal_display_width_js_1.measureTerminalDisplayWidth)(stripAnsiWelcome(text));
|
|
1345
|
+
const padding = Math.max(0, contentWidth - plainWidth);
|
|
1346
|
+
return `${applyWelcomeBorderColor("│")}${text}${" ".repeat(padding)}${applyWelcomeBorderColor("│")}`;
|
|
1347
|
+
}
|
|
1348
|
+
function formatWelcomeCenteredLine(contentWidth, text) {
|
|
1349
|
+
const plainWidth = (0, terminal_display_width_js_1.measureTerminalDisplayWidth)(stripAnsiWelcome(text));
|
|
1350
|
+
const padding = Math.max(0, contentWidth - plainWidth);
|
|
1351
|
+
const left = Math.floor(padding / 2);
|
|
1352
|
+
const right = padding - left;
|
|
1353
|
+
return `${applyWelcomeBorderColor("│")}${" ".repeat(left)}${text}${" ".repeat(right)}${applyWelcomeBorderColor("│")}`;
|
|
1354
|
+
}
|
|
1355
|
+
function computePromptCursorColumn(prompt, buffer, cursor) {
|
|
1356
|
+
return (0, terminal_display_width_js_1.measureTerminalDisplayWidth)(prompt) + (0, terminal_display_width_js_1.measureTerminalDisplayWidth)(buffer.slice(0, cursor));
|
|
1357
|
+
}
|
|
1358
|
+
function shouldRenderInteractiveTranscriptEvent(event) {
|
|
1359
|
+
if (event.category === "session")
|
|
1360
|
+
return false;
|
|
1361
|
+
if (event.category === "tool")
|
|
1362
|
+
return false;
|
|
1363
|
+
if (event.category === "compaction")
|
|
1364
|
+
return false;
|
|
1365
|
+
if (event.category === "permission")
|
|
1366
|
+
return false;
|
|
1367
|
+
return true;
|
|
1368
|
+
}
|
|
1369
|
+
function formatInteractiveToolEvent(event, startedParameters) {
|
|
1370
|
+
if (event.category !== "tool")
|
|
1371
|
+
return "";
|
|
1372
|
+
if (event.type === "tool_started") {
|
|
1373
|
+
return [
|
|
1374
|
+
formatInteractiveToolTitle(event.toolName, event.parameters),
|
|
1375
|
+
formatInteractiveToolPreview(event.toolName, event.parameters),
|
|
1376
|
+
]
|
|
1377
|
+
.filter((part) => part.length > 0)
|
|
1378
|
+
.join("\n");
|
|
1379
|
+
}
|
|
1380
|
+
if (event.type === "tool_completed") {
|
|
1381
|
+
return formatInteractiveToolResult(event.toolName, event.result, startedParameters);
|
|
1382
|
+
}
|
|
1383
|
+
if (event.type === "tool_denied") {
|
|
1384
|
+
return "";
|
|
1385
|
+
}
|
|
1386
|
+
return "";
|
|
1387
|
+
}
|
|
1388
|
+
function formatInteractivePermissionBlock(details, selectedIndex = 0) {
|
|
1389
|
+
const lines = formatInteractivePermissionBodyLines(details, selectedIndex);
|
|
1390
|
+
lines.splice(1, 0, "────────────────────────────────────────");
|
|
1391
|
+
return lines.join("\n");
|
|
1392
|
+
}
|
|
1393
|
+
function formatInteractiveFooter(state) {
|
|
1394
|
+
const separator = formatInteractiveInputSeparator(computeInteractiveFooterSeparatorWidth(state.terminalWidth));
|
|
1395
|
+
const lines = [];
|
|
1396
|
+
if (state.turnNotice !== null) {
|
|
1397
|
+
lines.push(formatTurnNotice(state.turnNotice));
|
|
1398
|
+
}
|
|
1399
|
+
lines.push(separator);
|
|
1400
|
+
if (state.permission !== null) {
|
|
1401
|
+
lines.push(...formatInteractivePermissionBodyLines(state.permission.details, state.permission.selectedIndex));
|
|
1402
|
+
const prompt = "choice [Enter/1/2/3]> ";
|
|
1403
|
+
lines.push(`${prompt}${state.buffer}`);
|
|
1404
|
+
lines.push(separator);
|
|
1405
|
+
return {
|
|
1406
|
+
block: lines.join("\n"),
|
|
1407
|
+
lines,
|
|
1408
|
+
cursorLineIndex: lines.length - 2,
|
|
1409
|
+
cursorColumn: computePromptCursorColumn(prompt, state.buffer, state.cursor),
|
|
1410
|
+
renderedWidth: Math.max(1, state.terminalWidth),
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
const hint = formatSlashSuggestionHint(state.suggestions, state.terminalWidth - computePromptCursorColumn(state.prompt, state.buffer, state.buffer.length));
|
|
1414
|
+
lines.push(`${state.prompt}${formatInteractivePromptBuffer(state.buffer, false)}${hint}`);
|
|
1415
|
+
lines.push(separator);
|
|
1416
|
+
return {
|
|
1417
|
+
block: lines.join("\n"),
|
|
1418
|
+
lines,
|
|
1419
|
+
cursorLineIndex: lines.length - 2,
|
|
1420
|
+
cursorColumn: computePromptCursorColumn(state.prompt, state.buffer, state.cursor),
|
|
1421
|
+
renderedWidth: Math.max(1, state.terminalWidth),
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
function movePermissionSelection(current, direction) {
|
|
1425
|
+
const size = 3;
|
|
1426
|
+
return (current + direction + size) % size;
|
|
1427
|
+
}
|
|
1428
|
+
function permissionDecisionFromSelection(selectedIndex) {
|
|
1429
|
+
if (selectedIndex === 1)
|
|
1430
|
+
return "allow_for_session";
|
|
1431
|
+
if (selectedIndex === 2)
|
|
1432
|
+
return "deny";
|
|
1433
|
+
return "allow_once";
|
|
1434
|
+
}
|
|
1435
|
+
function formatPermissionChoiceLine(index, selectedIndex, label) {
|
|
1436
|
+
const prefix = index === selectedIndex ? "❯" : " ";
|
|
1437
|
+
if (index === selectedIndex) {
|
|
1438
|
+
return `${prefix} ${theme_js_1.INTERACTIVE_THEME.selectedBg}${theme_js_1.INTERACTIVE_THEME.selectedFg}${index + 1}. ${label}${theme_js_1.INTERACTIVE_THEME.reset}`;
|
|
1439
|
+
}
|
|
1440
|
+
return `${prefix} ${index + 1}. ${label}`;
|
|
1441
|
+
}
|
|
1442
|
+
function formatInteractivePermissionBodyLines(details, selectedIndex) {
|
|
1443
|
+
return [
|
|
1444
|
+
formatInteractiveToolTitle(details.toolName, details.targetPath ? { file_path: details.targetPath } : {}),
|
|
1445
|
+
formatPermissionQuestion(details),
|
|
1446
|
+
formatPermissionChoiceLine(0, selectedIndex, "Yes"),
|
|
1447
|
+
formatPermissionChoiceLine(1, selectedIndex, "Yes, allow during this session"),
|
|
1448
|
+
formatPermissionChoiceLine(2, selectedIndex, "No"),
|
|
1449
|
+
];
|
|
1450
|
+
}
|
|
1451
|
+
function formatPermissionQuestion(details) {
|
|
1452
|
+
if (details.toolName === "write" || details.toolName === "edit") {
|
|
1453
|
+
const target = details.targetPath ? (0, node_path_1.basename)(details.targetPath) : "this file";
|
|
1454
|
+
const action = details.toolName === "write" ? "create or overwrite" : "edit";
|
|
1455
|
+
return `Do you want to ${action} ${target}?`;
|
|
1456
|
+
}
|
|
1457
|
+
return `Allow ${details.toolName} (${details.riskLevel})${details.reason ? ` — ${details.reason}` : ""}?`;
|
|
1458
|
+
}
|
|
1459
|
+
function formatInteractiveToolTitle(toolName, parameters = {}) {
|
|
1460
|
+
const name = interactiveToolDisplayName(toolName);
|
|
1461
|
+
const summary = summarizeToolParameters(toolName, parameters);
|
|
1462
|
+
return summary.length > 0 ? `⏺ ${name}(${summary})` : `⏺ ${name}`;
|
|
1463
|
+
}
|
|
1464
|
+
function formatInteractiveToolResult(toolName, result, startedParameters) {
|
|
1465
|
+
const lines = normalizeToolResultLines(toolName, result, startedParameters);
|
|
1466
|
+
if (lines.length === 0)
|
|
1467
|
+
return "";
|
|
1468
|
+
return lines.map((line, index) => `${index === 0 ? " ⎿" : " "} ${line}`).join("\n");
|
|
1469
|
+
}
|
|
1470
|
+
function formatInteractiveToolPreview(toolName, parameters) {
|
|
1471
|
+
if (toolName !== "write" && toolName !== "edit")
|
|
1472
|
+
return "";
|
|
1473
|
+
if (toolName === "edit") {
|
|
1474
|
+
const oldString = typeof parameters.old_string === "string" ? parameters.old_string : "";
|
|
1475
|
+
const newString = typeof parameters.new_string === "string" ? parameters.new_string : "";
|
|
1476
|
+
const diff = formatMiniDiffPreview(oldString, newString);
|
|
1477
|
+
if (diff.length > 0) {
|
|
1478
|
+
return diff;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
const previewSource = typeof parameters.content === "string" ? parameters.content : "";
|
|
1482
|
+
if (previewSource.trim().length === 0)
|
|
1483
|
+
return "";
|
|
1484
|
+
const previewLines = previewSource.trimEnd().split("\n").slice(0, 4);
|
|
1485
|
+
return [" │ Preview", ...previewLines.map((line) => ` │ ${truncatePreviewLine(line)}`)]
|
|
1486
|
+
.filter((line) => line.length > 0)
|
|
1487
|
+
.join("\n");
|
|
1488
|
+
}
|
|
1489
|
+
function interactiveToolDisplayName(toolName) {
|
|
1490
|
+
switch (toolName) {
|
|
1491
|
+
case "bash":
|
|
1492
|
+
return "Bash";
|
|
1493
|
+
case "write":
|
|
1494
|
+
return "Write";
|
|
1495
|
+
case "edit":
|
|
1496
|
+
return "Edit";
|
|
1497
|
+
case "read":
|
|
1498
|
+
return "Read";
|
|
1499
|
+
case "grep":
|
|
1500
|
+
return "Grep";
|
|
1501
|
+
case "find":
|
|
1502
|
+
return "Find";
|
|
1503
|
+
case "ls":
|
|
1504
|
+
return "LS";
|
|
1505
|
+
default:
|
|
1506
|
+
return toolName;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
function summarizeToolParameters(toolName, parameters) {
|
|
1510
|
+
if (toolName === "bash" && typeof parameters.command === "string") {
|
|
1511
|
+
return parameters.command;
|
|
1512
|
+
}
|
|
1513
|
+
const filePath = typeof parameters.file_path === "string"
|
|
1514
|
+
? parameters.file_path
|
|
1515
|
+
: typeof parameters.path === "string"
|
|
1516
|
+
? parameters.path
|
|
1517
|
+
: undefined;
|
|
1518
|
+
if (filePath) {
|
|
1519
|
+
return (0, node_path_1.basename)(filePath);
|
|
1520
|
+
}
|
|
1521
|
+
if (toolName === "grep" && typeof parameters.pattern === "string") {
|
|
1522
|
+
return parameters.pattern;
|
|
1523
|
+
}
|
|
1524
|
+
return "";
|
|
1525
|
+
}
|
|
1526
|
+
function normalizeToolResultLines(toolName, result, startedParameters) {
|
|
1527
|
+
if ((!result || result.trim().length === 0) && startedParameters) {
|
|
1528
|
+
if (toolName === "write" || toolName === "edit") {
|
|
1529
|
+
const target = typeof startedParameters.file_path === "string"
|
|
1530
|
+
? (0, node_path_1.basename)(startedParameters.file_path)
|
|
1531
|
+
: typeof startedParameters.path === "string"
|
|
1532
|
+
? (0, node_path_1.basename)(startedParameters.path)
|
|
1533
|
+
: "file";
|
|
1534
|
+
const previewSource = typeof startedParameters.content === "string"
|
|
1535
|
+
? startedParameters.content
|
|
1536
|
+
: typeof startedParameters.new_string === "string"
|
|
1537
|
+
? startedParameters.new_string
|
|
1538
|
+
: "";
|
|
1539
|
+
const lineCount = previewSource.length > 0 ? previewSource.split("\n").length : null;
|
|
1540
|
+
if (toolName === "write") {
|
|
1541
|
+
return [`Wrote ${lineCount ?? 1} lines to ${target}`];
|
|
1542
|
+
}
|
|
1543
|
+
const replacementCount = typeof startedParameters.old_string === "string" || typeof startedParameters.new_string === "string"
|
|
1544
|
+
? 1
|
|
1545
|
+
: null;
|
|
1546
|
+
return [
|
|
1547
|
+
`Applied edit to ${target}${replacementCount ? ` (${replacementCount} change)` : lineCount ? ` (${lineCount} lines)` : ""}`,
|
|
1548
|
+
];
|
|
1549
|
+
}
|
|
1550
|
+
if (toolName === "bash" && typeof startedParameters.command === "string") {
|
|
1551
|
+
return [`Ran: ${startedParameters.command}`];
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
if (!result || result.trim().length === 0)
|
|
1555
|
+
return [];
|
|
1556
|
+
if (toolName === "write" || toolName === "edit") {
|
|
1557
|
+
const lines = result.trimEnd().split("\n");
|
|
1558
|
+
return lines.slice(0, 4);
|
|
1559
|
+
}
|
|
1560
|
+
return result.trimEnd().split("\n").slice(0, 6);
|
|
1561
|
+
}
|
|
1562
|
+
function truncatePreviewLine(line) {
|
|
1563
|
+
return (0, terminal_display_width_js_1.measureTerminalDisplayWidth)(line) <= 72 ? line : `${line.slice(0, 69)}...`;
|
|
1564
|
+
}
|
|
1565
|
+
function formatMiniDiffPreview(oldString, newString) {
|
|
1566
|
+
if (oldString.trim().length === 0 && newString.trim().length === 0)
|
|
1567
|
+
return "";
|
|
1568
|
+
const removed = oldString
|
|
1569
|
+
.trimEnd()
|
|
1570
|
+
.split("\n")
|
|
1571
|
+
.filter((line) => line.length > 0)
|
|
1572
|
+
.slice(0, 2);
|
|
1573
|
+
const added = newString
|
|
1574
|
+
.trimEnd()
|
|
1575
|
+
.split("\n")
|
|
1576
|
+
.filter((line) => line.length > 0)
|
|
1577
|
+
.slice(0, 2);
|
|
1578
|
+
const lines = [" │ Diff"];
|
|
1579
|
+
lines.push(...removed.map((line) => ` - ${truncatePreviewLine(line)}`));
|
|
1580
|
+
lines.push(...added.map((line) => ` + ${truncatePreviewLine(line)}`));
|
|
1581
|
+
return lines.join("\n");
|
|
1582
|
+
}
|
|
1583
|
+
function computeSlashSuggestions(input, commands) {
|
|
1584
|
+
const trimmed = input.trimStart();
|
|
1585
|
+
if (!trimmed.startsWith("/"))
|
|
1586
|
+
return [];
|
|
1587
|
+
const body = trimmed.slice(1);
|
|
1588
|
+
if (body.includes(" "))
|
|
1589
|
+
return [];
|
|
1590
|
+
const query = body.toLowerCase();
|
|
1591
|
+
return commands
|
|
1592
|
+
.map((command) => command.name)
|
|
1593
|
+
.filter((name) => query.length === 0 || name.startsWith(query))
|
|
1594
|
+
.sort((a, b) => a.localeCompare(b))
|
|
1595
|
+
.slice(0, 6);
|
|
1596
|
+
}
|
|
1597
|
+
function formatSlashSuggestionHint(suggestions, remainingWidth) {
|
|
1598
|
+
if (suggestions.length === 0 || remainingWidth < 6)
|
|
1599
|
+
return "";
|
|
1600
|
+
const DIM = "\x1b[2m";
|
|
1601
|
+
const RESET = "\x1b[0m";
|
|
1602
|
+
let hint = "";
|
|
1603
|
+
for (const name of suggestions) {
|
|
1604
|
+
const segment = `${hint.length === 0 ? " " : " "}/${name}`;
|
|
1605
|
+
if ((0, terminal_display_width_js_1.measureTerminalDisplayWidth)(hint + segment) > remainingWidth) {
|
|
1606
|
+
break;
|
|
1607
|
+
}
|
|
1608
|
+
hint += segment;
|
|
1609
|
+
}
|
|
1610
|
+
return hint.length > 0 ? `${DIM}${hint}${RESET}` : "";
|
|
1611
|
+
}
|
|
1612
|
+
function acceptFirstSlashSuggestion(state, suggestions) {
|
|
1613
|
+
if (suggestions.length === 0)
|
|
1614
|
+
return null;
|
|
1615
|
+
if (state.cursor !== state.buffer.length)
|
|
1616
|
+
return null;
|
|
1617
|
+
const trimmed = state.buffer.trimStart();
|
|
1618
|
+
if (!trimmed.startsWith("/"))
|
|
1619
|
+
return null;
|
|
1620
|
+
if (trimmed.slice(1).includes(" "))
|
|
1621
|
+
return null;
|
|
1622
|
+
const nextBuffer = `/${suggestions[0]} `;
|
|
1623
|
+
return {
|
|
1624
|
+
buffer: nextBuffer,
|
|
1625
|
+
cursor: nextBuffer.length,
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
function formatTranscriptUserLine(content) {
|
|
1629
|
+
return `${theme_js_1.INTERACTIVE_THEME.promptBg}${theme_js_1.INTERACTIVE_THEME.userTranscriptFg} ${content} ${theme_js_1.INTERACTIVE_THEME.reset}`;
|
|
1630
|
+
}
|
|
1631
|
+
function formatTranscriptAssistantLine(content) {
|
|
1632
|
+
return `${theme_js_1.INTERACTIVE_THEME.assistantBullet}⏺${theme_js_1.INTERACTIVE_THEME.reset} ${content}`;
|
|
1633
|
+
}
|
|
1634
|
+
function formatInteractivePromptBuffer(content, plain = false) {
|
|
1635
|
+
if (plain)
|
|
1636
|
+
return content;
|
|
1637
|
+
return content;
|
|
1638
|
+
}
|
|
1639
|
+
function formatInteractiveInputSeparator(width) {
|
|
1640
|
+
return `${theme_js_1.INTERACTIVE_THEME.muted}${"─".repeat(Math.max(1, width))}${theme_js_1.INTERACTIVE_THEME.reset}`;
|
|
1641
|
+
}
|
|
1642
|
+
function computeInteractiveFooterSeparatorWidth(terminalWidth) {
|
|
1643
|
+
return Math.max(20, terminalWidth);
|
|
1644
|
+
}
|
|
1645
|
+
function computeFooterCursorColumn(width, cursorColumn) {
|
|
1646
|
+
const safeWidth = Math.max(1, width);
|
|
1647
|
+
return Math.max(0, cursorColumn % safeWidth);
|
|
1648
|
+
}
|
|
1649
|
+
function countRenderedTerminalRows(lines, width) {
|
|
1650
|
+
const safeWidth = Math.max(1, width);
|
|
1651
|
+
let total = 0;
|
|
1652
|
+
for (const line of lines) {
|
|
1653
|
+
const plain = stripAnsiWelcome(line);
|
|
1654
|
+
const visibleWidth = Math.max(1, (0, terminal_display_width_js_1.measureTerminalDisplayWidth)(plain));
|
|
1655
|
+
total += Math.max(1, Math.ceil(visibleWidth / safeWidth));
|
|
1656
|
+
}
|
|
1657
|
+
return total;
|
|
1658
|
+
}
|
|
1659
|
+
function computePromptCursorRowsUp(lines, width, cursorColumn) {
|
|
1660
|
+
const safeWidth = Math.max(1, width);
|
|
1661
|
+
const rowsBeforePrompt = countRenderedTerminalRows(lines.slice(0, 1), safeWidth);
|
|
1662
|
+
const promptRowOffset = Math.floor(Math.max(0, cursorColumn) / safeWidth);
|
|
1663
|
+
return rowsBeforePrompt + promptRowOffset;
|
|
1664
|
+
}
|
|
1665
|
+
function computeFooterCursorRowsUp(lines, width, cursorLineIndex, cursorColumn) {
|
|
1666
|
+
const safeWidth = Math.max(1, width);
|
|
1667
|
+
const rowsBeforeCursor = countRenderedTerminalRows(lines.slice(0, cursorLineIndex), safeWidth);
|
|
1668
|
+
const cursorRowOffset = Math.floor(Math.max(0, cursorColumn) / safeWidth);
|
|
1669
|
+
return rowsBeforeCursor + cursorRowOffset;
|
|
1670
|
+
}
|
|
1671
|
+
function computeFooterCursorRowsFromEnd(lines, width, cursorLineIndex, cursorColumn) {
|
|
1672
|
+
const totalRows = countRenderedTerminalRows(lines, width);
|
|
1673
|
+
const rowsUp = computeFooterCursorRowsUp(lines, width, cursorLineIndex, cursorColumn);
|
|
1674
|
+
return Math.max(0, totalRows - rowsUp - 1);
|
|
1675
|
+
}
|
|
1676
|
+
function computeInteractiveEphemeralRows(streaming, footer) {
|
|
1677
|
+
const footerRowsUp = footer === null
|
|
1678
|
+
? 0
|
|
1679
|
+
: computeFooterCursorRowsUp(footer.lines, footer.renderedWidth, footer.cursorLineIndex, footer.cursorColumn);
|
|
1680
|
+
const streamingRows = streaming === null ? 0 : countRenderedTerminalRows(streaming.lines, streaming.renderedWidth);
|
|
1681
|
+
return footerRowsUp + streamingRows;
|
|
1682
|
+
}
|
|
1683
|
+
function ansiCursorTo(row, column) {
|
|
1684
|
+
return `\x1b[${Math.max(1, row)};${Math.max(1, column)}H`;
|
|
1685
|
+
}
|
|
1686
|
+
function ansiSetScrollRegion(top, bottom) {
|
|
1687
|
+
return `\x1b[${Math.max(1, top)};${Math.max(1, bottom)}r`;
|
|
1688
|
+
}
|
|
1689
|
+
function ansiResetScrollRegion() {
|
|
1690
|
+
return "\x1b[r";
|
|
1691
|
+
}
|
|
1692
|
+
function ansiDisableAutoWrap() {
|
|
1693
|
+
return "\x1b[?7l";
|
|
1694
|
+
}
|
|
1695
|
+
function ansiEnableAutoWrap() {
|
|
1696
|
+
return "\x1b[?7h";
|
|
1697
|
+
}
|
|
1698
|
+
function fitTerminalLine(line, width) {
|
|
1699
|
+
const safeWidth = Math.max(1, width);
|
|
1700
|
+
const visibleWidth = (0, terminal_display_width_js_1.measureTerminalDisplayWidth)(stripAnsiWelcome(line));
|
|
1701
|
+
if (visibleWidth <= safeWidth) {
|
|
1702
|
+
return `${line}${" ".repeat(safeWidth - visibleWidth)}`;
|
|
1703
|
+
}
|
|
1704
|
+
const truncated = truncatePlainTerminalText(stripAnsiWelcome(line), safeWidth);
|
|
1705
|
+
return `${truncated}${" ".repeat(Math.max(0, safeWidth - (0, terminal_display_width_js_1.measureTerminalDisplayWidth)(truncated)))}`;
|
|
1706
|
+
}
|
|
1707
|
+
function truncatePlainTerminalText(text, width) {
|
|
1708
|
+
const safeWidth = Math.max(1, width);
|
|
1709
|
+
let output = "";
|
|
1710
|
+
let used = 0;
|
|
1711
|
+
for (const ch of text) {
|
|
1712
|
+
const charWidth = (0, terminal_display_width_js_1.measureTerminalDisplayWidth)(ch);
|
|
1713
|
+
if (used + charWidth > safeWidth) {
|
|
1714
|
+
break;
|
|
1715
|
+
}
|
|
1716
|
+
output += ch;
|
|
1717
|
+
used += charWidth;
|
|
1718
|
+
}
|
|
1719
|
+
return output;
|
|
1720
|
+
}
|
|
1721
|
+
function formatTurnNotice(kind) {
|
|
1722
|
+
const DIM = "\x1b[2m";
|
|
1723
|
+
const CYAN = "\x1b[36m";
|
|
1724
|
+
const RESET = "\x1b[0m";
|
|
1725
|
+
return kind === "thinking" ? `${DIM}${CYAN}· Thinking…${RESET}` : `${DIM}${CYAN}· Responding…${RESET}`;
|
|
1726
|
+
}
|
|
1727
|
+
function mergeStreamingText(existing, incoming) {
|
|
1728
|
+
if (incoming.length === 0)
|
|
1729
|
+
return existing;
|
|
1730
|
+
if (existing.length === 0)
|
|
1731
|
+
return incoming;
|
|
1732
|
+
if (incoming.startsWith(existing))
|
|
1733
|
+
return incoming;
|
|
1734
|
+
const embeddedExistingIndex = incoming.indexOf(existing);
|
|
1735
|
+
if (embeddedExistingIndex >= 0 && embeddedExistingIndex <= 8) {
|
|
1736
|
+
return incoming.slice(embeddedExistingIndex);
|
|
1737
|
+
}
|
|
1738
|
+
if (existing.endsWith(incoming))
|
|
1739
|
+
return existing;
|
|
1740
|
+
const trimmedIncoming = incoming.trimStart();
|
|
1741
|
+
if (trimmedIncoming.startsWith(existing))
|
|
1742
|
+
return trimmedIncoming;
|
|
1743
|
+
const maxOverlap = Math.min(existing.length, incoming.length);
|
|
1744
|
+
for (let overlap = maxOverlap; overlap > 0; overlap -= 1) {
|
|
1745
|
+
if (existing.endsWith(incoming.slice(0, overlap))) {
|
|
1746
|
+
return `${existing}${incoming.slice(overlap)}`;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
const trimmedMaxOverlap = Math.min(existing.length, trimmedIncoming.length);
|
|
1750
|
+
for (let overlap = trimmedMaxOverlap; overlap > 0; overlap -= 1) {
|
|
1751
|
+
if (existing.endsWith(trimmedIncoming.slice(0, overlap))) {
|
|
1752
|
+
return `${existing}${trimmedIncoming.slice(overlap)}`;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
return `${existing}${incoming}`;
|
|
1756
|
+
}
|
|
1757
|
+
function wrapTranscriptContent(content, width) {
|
|
1758
|
+
if (content.length === 0) {
|
|
1759
|
+
return [""];
|
|
1760
|
+
}
|
|
1761
|
+
const lines = [];
|
|
1762
|
+
let current = "";
|
|
1763
|
+
let currentWidth = 0;
|
|
1764
|
+
for (const ch of content.replace(/\r\n/g, "\n")) {
|
|
1765
|
+
if (ch === "\n") {
|
|
1766
|
+
lines.push(current);
|
|
1767
|
+
current = "";
|
|
1768
|
+
currentWidth = 0;
|
|
1769
|
+
continue;
|
|
1770
|
+
}
|
|
1771
|
+
const charWidth = (0, terminal_display_width_js_1.measureTerminalDisplayWidth)(ch);
|
|
1772
|
+
if (currentWidth + charWidth > width && current.length > 0) {
|
|
1773
|
+
lines.push(current);
|
|
1774
|
+
current = ch;
|
|
1775
|
+
currentWidth = charWidth;
|
|
1776
|
+
continue;
|
|
1777
|
+
}
|
|
1778
|
+
current += ch;
|
|
1779
|
+
currentWidth += charWidth;
|
|
1780
|
+
}
|
|
1781
|
+
lines.push(current);
|
|
1782
|
+
return lines;
|
|
1783
|
+
}
|
|
1784
|
+
function computeVisibleTranscriptLines(blocks, width, maxRows) {
|
|
1785
|
+
if (maxRows <= 0 || blocks.length === 0) {
|
|
1786
|
+
return [];
|
|
1787
|
+
}
|
|
1788
|
+
const flattened = flattenTranscriptLines(blocks, width);
|
|
1789
|
+
if (flattened.length <= maxRows) {
|
|
1790
|
+
return flattened;
|
|
1791
|
+
}
|
|
1792
|
+
return flattened.slice(flattened.length - maxRows);
|
|
1793
|
+
}
|
|
1794
|
+
function computeTranscriptDisplayRows(blocks, width) {
|
|
1795
|
+
return flattenTranscriptLines(blocks, width).length;
|
|
1796
|
+
}
|
|
1797
|
+
function materializeAssistantTranscriptBlock(buffer) {
|
|
1798
|
+
if (buffer.length === 0) {
|
|
1799
|
+
return null;
|
|
1800
|
+
}
|
|
1801
|
+
return formatTranscriptAssistantLine(buffer);
|
|
1802
|
+
}
|
|
1803
|
+
function appendAssistantTranscriptBlock(blocks, assistantBlock) {
|
|
1804
|
+
const lastNonEmptyBlock = [...blocks].reverse().find((block) => block.length > 0);
|
|
1805
|
+
if (lastNonEmptyBlock && isTranscriptUserBlock(lastNonEmptyBlock)) {
|
|
1806
|
+
return [...blocks, "", assistantBlock];
|
|
1807
|
+
}
|
|
1808
|
+
return [...blocks, assistantBlock];
|
|
1809
|
+
}
|
|
1810
|
+
function computeFooterStartRow(welcomeLineCount, terminalHeight, footerHeight, transcriptRows) {
|
|
1811
|
+
const naturalStartRow = welcomeLineCount + 1 + Math.max(0, transcriptRows);
|
|
1812
|
+
const bottomAnchoredStartRow = Math.max(1, terminalHeight - footerHeight + 1);
|
|
1813
|
+
return Math.min(naturalStartRow, bottomAnchoredStartRow);
|
|
1814
|
+
}
|
|
1815
|
+
function isFooterBottomAnchored(startRow, terminalHeight, footerHeight) {
|
|
1816
|
+
return startRow === Math.max(1, terminalHeight - footerHeight + 1);
|
|
1817
|
+
}
|
|
1818
|
+
function flattenTranscriptLines(blocks, width) {
|
|
1819
|
+
const flattened = [];
|
|
1820
|
+
for (const block of blocks) {
|
|
1821
|
+
for (const logicalLine of block.split("\n")) {
|
|
1822
|
+
flattened.push(...wrapTranscriptContent(logicalLine, width));
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
return flattened;
|
|
1826
|
+
}
|
|
1827
|
+
function isTranscriptUserBlock(block) {
|
|
1828
|
+
return block.startsWith(`${theme_js_1.INTERACTIVE_THEME.promptBg}${theme_js_1.INTERACTIVE_THEME.userTranscriptFg} `);
|
|
1829
|
+
}
|
|
1830
|
+
//# sourceMappingURL=mode-dispatch.js.map
|