opencode-miniterm 1.0.4 → 1.0.6

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/src/index.ts CHANGED
@@ -6,7 +6,7 @@ import { mkdir } from "node:fs/promises";
6
6
  import { glob } from "node:fs/promises";
7
7
  import { stat } from "node:fs/promises";
8
8
  import { open } from "node:fs/promises";
9
- import readline from "node:readline";
9
+ import readline, { type Key } from "node:readline";
10
10
  import * as ansi from "./ansi";
11
11
  import agentsCommand from "./commands/agents";
12
12
  import debugCommand from "./commands/debug";
@@ -46,6 +46,7 @@ const SLASH_COMMANDS = [
46
46
  runCommand,
47
47
  ];
48
48
 
49
+ let server: Awaited<ReturnType<typeof createOpencodeServer>> | undefined;
49
50
  let client: ReturnType<typeof createOpencodeClient>;
50
51
 
51
52
  let processing = true;
@@ -92,7 +93,6 @@ async function main() {
92
93
 
93
94
  console.log(`\n${ansi.BRIGHT_BLACK}Connecting to OpenCode server...${ansi.RESET}\n`);
94
95
 
95
- let server: Awaited<ReturnType<typeof createOpencodeServer>> | undefined;
96
96
  try {
97
97
  server = await createOpencodeServer();
98
98
  } catch {
@@ -112,10 +112,8 @@ async function main() {
112
112
  });
113
113
 
114
114
  process.on("SIGINT", () => {
115
- console.log("\nShutting down...");
116
- saveConfig();
117
- server?.close();
118
- process.exit(0);
115
+ process.stdout.write("\n");
116
+ shutdown();
119
117
  });
120
118
 
121
119
  try {
@@ -135,12 +133,11 @@ async function main() {
135
133
 
136
134
  await updateSessionTitle();
137
135
 
138
- const sessionHistory = await loadSessionHistory();
139
-
140
- const activeDisplay = await getActiveDisplay(client);
136
+ history = await loadSessionHistory();
141
137
 
142
138
  process.stdout.write(`${ansi.CLEAR_SCREEN_UP}${ansi.CLEAR_FROM_CURSOR}`);
143
139
  process.stdout.write(ansi.CURSOR_HOME);
140
+ const activeDisplay = await getActiveDisplay(client);
144
141
  console.log(activeDisplay);
145
142
  if (!isNewSession) {
146
143
  console.log("Resumed last session");
@@ -150,311 +147,327 @@ async function main() {
150
147
 
151
148
  const rl = readline.createInterface({
152
149
  input: process.stdin,
153
- output: process.stdout,
150
+ output: undefined,
154
151
  });
155
152
 
156
153
  readline.emitKeypressEvents(process.stdin);
157
154
  if (process.stdin.setRawMode) {
158
155
  process.stdin.setRawMode(true);
159
156
  }
157
+ process.stdout.write(ansi.DISABLE_LINE_WRAP);
160
158
 
161
- let inputBuffer = "";
162
- let cursorPosition = 0;
163
- let completions: string[] = [];
164
- let history: string[] = sessionHistory;
165
- let historyIndex = history.length;
166
- let selectedCompletion = 0;
167
- let showCompletions = false;
168
- let completionCycling = false;
169
- let lastSpaceTime = 0;
170
- let currentInputBuffer: string | null = null;
171
-
172
- const getCompletions = async (text: string): Promise<string[]> => {
173
- if (text.startsWith("/")) {
174
- return ["/help", ...SLASH_COMMANDS.map((c) => c.name)].filter((cmd) =>
175
- cmd.startsWith(text),
176
- );
177
- }
159
+ process.stdin.on("keypress", async (str, key) => {
160
+ handleKeyPress(str, key);
161
+ });
178
162
 
179
- const atMatch = text.match(/(@[^\s]*)$/);
180
- if (atMatch) {
181
- const prefix = atMatch[0]!;
182
- const searchPattern = prefix.slice(1);
183
- const pattern = searchPattern.includes("/")
184
- ? searchPattern + "*"
185
- : "**/" + searchPattern + "*";
186
- const files = await getFileCompletions(pattern);
187
- return files.map((file: string) => text.replace(/@[^\s]*$/, "@" + file));
188
- }
163
+ writePrompt();
164
+ } catch (error: any) {
165
+ console.error("Error:", error.message);
166
+ server?.close();
167
+ process.exit(1);
168
+ }
169
+ }
189
170
 
190
- return [];
191
- };
192
-
193
- let oldWrappedRows = 0;
194
- const renderLine = (): void => {
195
- const consoleWidth = process.stdout.columns || 80;
196
- const totalLength = 2 + inputBuffer.length + 1;
197
- const wrappedRows = Math.floor(totalLength / consoleWidth);
198
- readline.cursorTo(process.stdout, 0);
199
- if (oldWrappedRows > 0) {
200
- readline.moveCursor(process.stdout, 0, -oldWrappedRows);
201
- }
202
- readline.clearScreenDown(process.stdout);
203
- oldWrappedRows = wrappedRows;
171
+ // ====================
172
+ // HANDLE INPUT
173
+ // ====================
204
174
 
205
- writePrompt();
206
- process.stdout.write(inputBuffer);
175
+ let inputBuffer = "";
176
+ let cursorPosition = 0;
177
+ let completions: string[] = [];
178
+ let history: string[] = [];
179
+ let historyIndex = history.length;
180
+ let selectedCompletion = 0;
181
+ let completionCycling = false;
182
+ let lastSpaceTime = 0;
183
+ let currentInputBuffer: string | null = null;
184
+
185
+ let oldWrappedRows = 0;
186
+ function renderLine(): void {
187
+ const consoleWidth = process.stdout.columns || 80;
188
+
189
+ readline.cursorTo(process.stdout, 0);
190
+ if (oldWrappedRows > 0) {
191
+ readline.moveCursor(process.stdout, 0, -oldWrappedRows);
192
+ }
193
+ readline.clearScreenDown(process.stdout);
207
194
 
208
- const totalPosition = 2 + cursorPosition;
209
- const targetRow = Math.floor(totalPosition / consoleWidth);
210
- const targetCol = totalPosition % consoleWidth;
195
+ writePrompt();
211
196
 
212
- const endCol = (2 + inputBuffer.length) % consoleWidth;
213
- const endRow = Math.floor((2 + inputBuffer.length) / consoleWidth);
197
+ let currentCol = 2;
198
+ let row = 0;
199
+ for (let i = 0; i < inputBuffer.length; i++) {
200
+ if (currentCol >= consoleWidth) {
201
+ process.stdout.write("\n");
202
+ currentCol = 0;
203
+ row++;
204
+ }
205
+ process.stdout.write(inputBuffer[i]!);
206
+ currentCol++;
207
+ }
208
+ oldWrappedRows = row;
209
+
210
+ let targetRow = 0;
211
+ let targetCol = 0;
212
+ let pos = 2;
213
+ for (let i = 0; i < cursorPosition; i++) {
214
+ if (pos >= consoleWidth) {
215
+ targetRow++;
216
+ pos = 0;
217
+ }
218
+ pos++;
219
+ }
220
+ targetCol = pos;
214
221
 
215
- const deltaCol = targetCol - endCol;
216
- let deltaRow = targetRow - endRow;
217
- if (deltaCol !== 0 && endCol === 0) {
218
- deltaRow += 1;
219
- }
222
+ readline.cursorTo(process.stdout, 0);
223
+ if (targetRow > 0) {
224
+ process.stdout.write(`\x1b[${targetRow}B`);
225
+ }
226
+ readline.cursorTo(process.stdout, targetCol);
227
+ }
220
228
 
221
- readline.moveCursor(process.stdout, deltaCol, deltaRow);
222
- };
229
+ async function handleKeyPress(str: string, key: Key) {
230
+ if (key.ctrl && key.name === "c") {
231
+ process.stdout.write("\n");
232
+ shutdown();
233
+ return;
234
+ }
223
235
 
224
- const handleTab = async (): Promise<void> => {
225
- const potentialCompletions = await getCompletions(inputBuffer);
236
+ for (let command of SLASH_COMMANDS) {
237
+ if (command.running && command.handleKey) {
238
+ await command.handleKey(client, key, str);
239
+ return;
240
+ }
241
+ }
226
242
 
227
- if (potentialCompletions.length === 0) {
228
- completionCycling = false;
229
- return;
243
+ switch (key.name) {
244
+ case "up": {
245
+ if (historyIndex === history.length) {
246
+ currentInputBuffer = inputBuffer;
230
247
  }
231
-
232
- if (!completionCycling) {
233
- completions = potentialCompletions;
234
- selectedCompletion = 0;
235
- completionCycling = true;
236
- inputBuffer = completions[0]!;
248
+ if (history.length > 0) {
249
+ if (historyIndex > 0) {
250
+ historyIndex--;
251
+ inputBuffer = history[historyIndex]!;
252
+ } else {
253
+ historyIndex = Math.max(-1, historyIndex - 1);
254
+ inputBuffer = "";
255
+ }
237
256
  cursorPosition = inputBuffer.length;
238
257
  renderLine();
239
- } else {
240
- selectedCompletion = (selectedCompletion + 1) % completions.length;
241
- inputBuffer = completions[selectedCompletion]!;
258
+ }
259
+ return;
260
+ }
261
+ case "down": {
262
+ if (history.length > 0) {
263
+ if (historyIndex < history.length - 1) {
264
+ historyIndex++;
265
+ inputBuffer = history[historyIndex]!;
266
+ } else {
267
+ historyIndex = history.length;
268
+ inputBuffer = currentInputBuffer || "";
269
+ currentInputBuffer = null;
270
+ }
242
271
  cursorPosition = inputBuffer.length;
243
272
  renderLine();
244
273
  }
245
- };
274
+ return;
275
+ }
276
+ case "tab": {
277
+ if (!completionCycling) {
278
+ await handleTab();
279
+ }
280
+ if (completionCycling && completions.length > 0) {
281
+ await handleTab();
282
+ }
283
+ return;
284
+ }
285
+ case "escape": {
286
+ if (isRequestActive) {
287
+ if (state.sessionID) {
288
+ client.session.abort({ path: { id: state.sessionID } }).catch(() => {});
289
+ }
290
+ stopAnimation();
291
+ process.stdout.write(ansi.CURSOR_SHOW);
292
+ process.stdout.write(`\r ${ansi.BRIGHT_BLACK}Cancelled request${ansi.RESET}\n`);
293
+ writePrompt();
294
+ isRequestActive = false;
295
+ } else {
296
+ inputBuffer = "";
297
+ cursorPosition = 0;
298
+ currentInputBuffer = null;
299
+ renderLine();
300
+ }
301
+ return;
302
+ }
303
+ case "return": {
304
+ await acceptInput();
305
+ return;
306
+ }
307
+ case "backspace": {
308
+ if (cursorPosition > 0) {
309
+ inputBuffer = inputBuffer.slice(0, cursorPosition - 1) + inputBuffer.slice(cursorPosition);
310
+ cursorPosition--;
311
+ currentInputBuffer = null;
312
+ }
313
+ break;
314
+ }
315
+ case "delete": {
316
+ if (cursorPosition < inputBuffer.length) {
317
+ inputBuffer = inputBuffer.slice(0, cursorPosition) + inputBuffer.slice(cursorPosition + 1);
318
+ currentInputBuffer = null;
319
+ }
320
+ break;
321
+ }
322
+ case "left": {
323
+ if (key.meta) {
324
+ cursorPosition = findPreviousWordBoundary(inputBuffer, cursorPosition);
325
+ } else if (cursorPosition > 0) {
326
+ cursorPosition--;
327
+ }
328
+ break;
329
+ }
330
+ case "right": {
331
+ if (key.meta) {
332
+ cursorPosition = findNextWordBoundary(inputBuffer, cursorPosition);
333
+ } else if (cursorPosition < inputBuffer.length) {
334
+ cursorPosition++;
335
+ }
336
+ break;
337
+ }
338
+ default: {
339
+ if (str === " ") {
340
+ const now = Date.now();
341
+ if (
342
+ now - lastSpaceTime < 500 &&
343
+ cursorPosition > 0 &&
344
+ inputBuffer[cursorPosition - 1] === " "
345
+ ) {
346
+ inputBuffer =
347
+ inputBuffer.slice(0, cursorPosition - 1) + ". " + inputBuffer.slice(cursorPosition);
348
+ cursorPosition += 1;
349
+ } else {
350
+ inputBuffer =
351
+ inputBuffer.slice(0, cursorPosition) + str + inputBuffer.slice(cursorPosition);
352
+ cursorPosition += str.length;
353
+ }
354
+ lastSpaceTime = now;
355
+ } else if (str) {
356
+ inputBuffer =
357
+ inputBuffer.slice(0, cursorPosition) + str + inputBuffer.slice(cursorPosition);
358
+ cursorPosition += str.length;
359
+ }
360
+ currentInputBuffer = null;
361
+ }
362
+ }
246
363
 
247
- const acceptInput = async (): Promise<void> => {
248
- process.stdout.write("\n");
364
+ completionCycling = false;
365
+ completions = [];
366
+ renderLine();
367
+ }
249
368
 
250
- const input = inputBuffer.trim();
369
+ async function handleTab(): Promise<void> {
370
+ const potentialCompletions = await getCompletions(inputBuffer);
251
371
 
252
- inputBuffer = "";
253
- cursorPosition = 0;
254
- showCompletions = false;
255
- completionCycling = false;
256
- completions = [];
257
- currentInputBuffer = null;
372
+ if (potentialCompletions.length === 0) {
373
+ completionCycling = false;
374
+ return;
375
+ }
258
376
 
259
- if (input) {
260
- if (history[history.length - 1] !== input) {
261
- history.push(input);
262
- }
263
- historyIndex = history.length;
264
- try {
265
- if (input === "/help") {
266
- const maxCommandLength = Math.max(...SLASH_COMMANDS.map((c) => c.name.length));
267
- for (const cmd of SLASH_COMMANDS) {
268
- const padding = " ".repeat(maxCommandLength - cmd.name.length + 2);
269
- console.log(
270
- ` ${ansi.BRIGHT_WHITE}${cmd.name}${ansi.RESET}${padding}${ansi.BRIGHT_BLACK}${cmd.description}${ansi.RESET}`,
271
- );
272
- }
273
- console.log();
274
- return;
275
- } else if (input.startsWith("/")) {
276
- const parts = input.match(/(\/[^\s]+)\s*(.*)/)!;
277
- if (parts) {
278
- const commandName = parts[1];
279
- const extra = parts[2]?.trim();
280
- for (let command of SLASH_COMMANDS) {
281
- if (command.name === commandName) {
282
- await command.run(client, state, extra);
283
- return;
284
- }
285
- }
286
- }
287
- return;
288
- }
377
+ if (!completionCycling) {
378
+ completions = potentialCompletions;
379
+ selectedCompletion = 0;
380
+ completionCycling = true;
381
+ inputBuffer = completions[0]!;
382
+ cursorPosition = inputBuffer.length;
383
+ renderLine();
384
+ } else {
385
+ selectedCompletion = (selectedCompletion + 1) % completions.length;
386
+ inputBuffer = completions[selectedCompletion]!;
387
+ cursorPosition = inputBuffer.length;
388
+ renderLine();
389
+ }
390
+ }
289
391
 
290
- isRequestActive = true;
291
- process.stdout.write(ansi.CURSOR_HIDE);
292
- startAnimation();
293
- if (isLoggingEnabled()) {
294
- console.log(`📝 ${ansi.BRIGHT_BLACK}Logging to ${getLogDir()}\n${ansi.RESET}`);
295
- }
296
- await sendMessage(state.sessionID, input);
297
- isRequestActive = false;
298
- } catch (error: any) {
299
- isRequestActive = false;
300
- if (error.message !== "Request cancelled") {
301
- stopAnimation();
302
- console.error("Error:", error.message);
303
- }
304
- }
305
- }
392
+ async function getCompletions(text: string): Promise<string[]> {
393
+ if (text.startsWith("/")) {
394
+ return ["/help", ...SLASH_COMMANDS.map((c) => c.name)].filter((cmd) => cmd.startsWith(text));
395
+ }
306
396
 
307
- if (!SLASH_COMMANDS.find((c) => c.running)) {
308
- writePrompt();
309
- }
310
- };
397
+ const atMatch = text.match(/(@[^\s]*)$/);
398
+ if (atMatch) {
399
+ const prefix = atMatch[0]!;
400
+ const searchPattern = prefix.slice(1);
401
+ const pattern = searchPattern.includes("/") ? searchPattern + "*" : "**/" + searchPattern + "*";
402
+ const files = await getFileCompletions(pattern);
403
+ return files.map((file: string) => text.replace(/@[^\s]*$/, "@" + file));
404
+ }
311
405
 
312
- process.stdin.on("keypress", async (str, key) => {
313
- for (let command of SLASH_COMMANDS) {
314
- if (command.running && command.handleKey) {
315
- await command.handleKey(client, key, str);
316
- return;
317
- }
318
- }
406
+ return [];
407
+ }
319
408
 
320
- switch (key.name) {
321
- case "up": {
322
- if (historyIndex === history.length) {
323
- currentInputBuffer = inputBuffer;
324
- }
325
- if (history.length > 0) {
326
- if (historyIndex > 0) {
327
- historyIndex--;
328
- inputBuffer = history[historyIndex]!;
329
- } else {
330
- historyIndex = Math.max(-1, historyIndex - 1);
331
- inputBuffer = "";
332
- }
333
- cursorPosition = inputBuffer.length;
334
- renderLine();
335
- }
336
- return;
337
- }
338
- case "down": {
339
- if (history.length > 0) {
340
- if (historyIndex < history.length - 1) {
341
- historyIndex++;
342
- inputBuffer = history[historyIndex]!;
343
- } else {
344
- historyIndex = history.length;
345
- inputBuffer = currentInputBuffer || "";
346
- currentInputBuffer = null;
347
- }
348
- cursorPosition = inputBuffer.length;
349
- renderLine();
350
- }
351
- return;
352
- }
353
- case "tab": {
354
- if (!completionCycling) {
355
- await handleTab();
356
- }
357
- if (completionCycling && completions.length > 0) {
358
- await handleTab();
359
- }
360
- return;
361
- }
362
- case "escape": {
363
- if (isRequestActive) {
364
- if (state.sessionID) {
365
- client.session.abort({ path: { id: state.sessionID } }).catch(() => {});
366
- }
367
- stopAnimation();
368
- process.stdout.write(ansi.CURSOR_SHOW);
369
- process.stdout.write(`\r ${ansi.BRIGHT_BLACK}Cancelled request${ansi.RESET}\n`);
370
- writePrompt();
371
- isRequestActive = false;
372
- } else {
373
- inputBuffer = "";
374
- cursorPosition = 0;
375
- currentInputBuffer = null;
376
- renderLine();
377
- }
378
- return;
379
- }
380
- case "return": {
381
- await acceptInput();
382
- return;
383
- }
384
- case "backspace": {
385
- if (cursorPosition > 0) {
386
- inputBuffer =
387
- inputBuffer.slice(0, cursorPosition - 1) + inputBuffer.slice(cursorPosition);
388
- cursorPosition--;
389
- currentInputBuffer = null;
390
- }
391
- break;
392
- }
393
- case "delete": {
394
- if (cursorPosition < inputBuffer.length) {
395
- inputBuffer =
396
- inputBuffer.slice(0, cursorPosition) + inputBuffer.slice(cursorPosition + 1);
397
- currentInputBuffer = null;
398
- }
399
- break;
400
- }
401
- case "left": {
402
- if (key.meta) {
403
- cursorPosition = findPreviousWordBoundary(inputBuffer, cursorPosition);
404
- } else if (cursorPosition > 0) {
405
- cursorPosition--;
406
- }
407
- break;
408
- }
409
- case "right": {
410
- if (key.meta) {
411
- cursorPosition = findNextWordBoundary(inputBuffer, cursorPosition);
412
- } else if (cursorPosition < inputBuffer.length) {
413
- cursorPosition++;
414
- }
415
- break;
409
+ async function acceptInput(): Promise<void> {
410
+ process.stdout.write("\n");
411
+
412
+ const input = inputBuffer.trim();
413
+
414
+ inputBuffer = "";
415
+ cursorPosition = 0;
416
+ completionCycling = false;
417
+ completions = [];
418
+ currentInputBuffer = null;
419
+
420
+ if (input) {
421
+ if (history[history.length - 1] !== input) {
422
+ history.push(input);
423
+ }
424
+ historyIndex = history.length;
425
+ try {
426
+ if (input === "/help") {
427
+ process.stdout.write("\n");
428
+ const maxCommandLength = Math.max(...SLASH_COMMANDS.map((c) => c.name.length));
429
+ for (const cmd of SLASH_COMMANDS) {
430
+ const padding = " ".repeat(maxCommandLength - cmd.name.length + 2);
431
+ console.log(
432
+ ` ${ansi.BRIGHT_WHITE}${cmd.name}${ansi.RESET}${padding}${ansi.BRIGHT_BLACK}${cmd.description}${ansi.RESET}`,
433
+ );
416
434
  }
417
- default: {
418
- if (str) {
419
- if (str === " ") {
420
- const now = Date.now();
421
- if (
422
- now - lastSpaceTime < 500 &&
423
- cursorPosition > 0 &&
424
- inputBuffer[cursorPosition - 1] === " "
425
- ) {
426
- inputBuffer =
427
- inputBuffer.slice(0, cursorPosition - 1) +
428
- ". " +
429
- inputBuffer.slice(cursorPosition);
430
- cursorPosition += 1;
431
- } else {
432
- inputBuffer =
433
- inputBuffer.slice(0, cursorPosition) + str + inputBuffer.slice(cursorPosition);
434
- cursorPosition += str.length;
435
- }
436
- lastSpaceTime = now;
437
- } else {
438
- inputBuffer =
439
- inputBuffer.slice(0, cursorPosition) + str + inputBuffer.slice(cursorPosition);
440
- cursorPosition += str.length;
435
+ console.log();
436
+ writePrompt();
437
+ return;
438
+ } else if (input.startsWith("/")) {
439
+ const parts = input.match(/(\/[^\s]+)\s*(.*)/)!;
440
+ if (parts) {
441
+ const commandName = parts[1];
442
+ const extra = parts[2]?.trim();
443
+ for (let command of SLASH_COMMANDS) {
444
+ if (command.name === commandName) {
445
+ process.stdout.write("\n");
446
+ await command.run(client, state, extra);
447
+ writePrompt();
448
+ return;
441
449
  }
442
- currentInputBuffer = null;
443
450
  }
444
451
  }
452
+ return;
445
453
  }
446
454
 
447
- showCompletions = false;
448
- completionCycling = false;
449
- completions = [];
450
- renderLine();
451
- });
452
-
453
- writePrompt();
454
- } catch (error: any) {
455
- console.error("Error:", error.message);
456
- server?.close();
457
- process.exit(1);
455
+ isRequestActive = true;
456
+ process.stdout.write("\n");
457
+ process.stdout.write(ansi.CURSOR_HIDE);
458
+ startAnimation();
459
+ if (isLoggingEnabled()) {
460
+ console.log(`📝 ${ansi.BRIGHT_BLACK}Logging to ${getLogDir()}\n${ansi.RESET}`);
461
+ }
462
+ await sendMessage(state.sessionID, input);
463
+ isRequestActive = false;
464
+ } catch (error: any) {
465
+ isRequestActive = false;
466
+ if (error.message !== "Request cancelled") {
467
+ stopAnimation();
468
+ console.error("Error:", error.message);
469
+ }
470
+ }
458
471
  }
459
472
  }
460
473
 
@@ -604,11 +617,6 @@ async function sendMessage(sessionID: string, message: string) {
604
617
  console.log(` ${ansi.BRIGHT_BLACK}Completed in ${durationText}${ansi.RESET}\n`);
605
618
 
606
619
  writePrompt();
607
-
608
- // HACK:
609
- setTimeout(() => {
610
- readline.cursorTo(process.stdout, 2);
611
- }, 200);
612
620
  } catch (error: any) {
613
621
  throw error;
614
622
  } finally {
@@ -672,7 +680,6 @@ async function processEvent(event: Event): Promise<void> {
672
680
  retryInterval = null;
673
681
  }
674
682
  writePrompt();
675
- readline.cursorTo(process.stdout, 2);
676
683
  }
677
684
  if (event.type === "session.status" && event.properties.status.type === "retry") {
678
685
  const message = event.properties.status.message;
@@ -799,7 +806,7 @@ async function processToolUse(part: Part) {
799
806
  const toolPart = part as ToolPart;
800
807
  const toolName = toolPart.tool || "unknown";
801
808
  const toolInput = toolPart.state.input["description"] || toolPart.state.input["filePath"] || {};
802
- const toolText = `${ansi.BRIGHT_BLACK}$${ansi.RESET} ${toolName}: ${ansi.BRIGHT_BLACK}${toolInput}${ansi.RESET}`;
809
+ const toolText = `$ ${toolName}: ${ansi.BRIGHT_BLACK}${toolInput}${ansi.RESET}`;
803
810
 
804
811
  if (state.accumulatedResponse[state.accumulatedResponse.length - 1]?.title === "tool") {
805
812
  state.accumulatedResponse[state.accumulatedResponse.length - 1]!.text = toolText;
@@ -883,6 +890,18 @@ function findLastPart(title: string) {
883
890
  }
884
891
  }
885
892
 
893
+ function shutdown() {
894
+ if (process.stdin.setRawMode) {
895
+ process.stdin.setRawMode(false);
896
+ }
897
+ process.stdin.destroy();
898
+ process.stdout.write(ansi.ENABLE_LINE_WRAP);
899
+ saveConfig();
900
+ server?.close();
901
+ console.log(`\n${ansi.BRIGHT_BLACK}Goodbye!${ansi.RESET}`);
902
+ process.exit(0);
903
+ }
904
+
886
905
  // ====================
887
906
  // USER INTERFACE
888
907
  // ====================