opencode-miniterm 1.0.14 → 1.0.16
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/bun.lock +1 -1
- package/package.json +2 -2
- package/src/ansi.ts +1 -0
- package/src/commands/agents.ts +5 -5
- package/src/commands/debug.ts +3 -4
- package/src/commands/details.ts +2 -4
- package/src/commands/diff.ts +3 -5
- package/src/commands/exit.ts +3 -6
- package/src/commands/init.ts +4 -7
- package/src/commands/log.ts +2 -8
- package/src/commands/models.ts +5 -6
- package/src/commands/new.ts +8 -11
- package/src/commands/page.ts +3 -5
- package/src/commands/quit.ts +3 -15
- package/src/commands/run.ts +2 -4
- package/src/commands/sessions.ts +11 -13
- package/src/commands/undo.ts +5 -7
- package/src/index.ts +19 -979
- package/src/input.ts +453 -0
- package/src/logs.ts +59 -0
- package/src/render.ts +23 -20
- package/src/server.ts +399 -0
- package/src/types.ts +23 -4
- package/src/utils.ts +17 -0
- package/test/render.test.ts +4 -2
package/src/input.ts
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import type { Part } from "@opencode-ai/sdk";
|
|
2
|
+
import { glob } from "node:fs/promises";
|
|
3
|
+
import { stat } from "node:fs/promises";
|
|
4
|
+
import readline, { type Key } from "node:readline";
|
|
5
|
+
import * as ansi from "./ansi";
|
|
6
|
+
import agentsCommand from "./commands/agents";
|
|
7
|
+
import debugCommand from "./commands/debug";
|
|
8
|
+
import detailsCommand from "./commands/details";
|
|
9
|
+
import diffCommand from "./commands/diff";
|
|
10
|
+
import exitCommand from "./commands/exit";
|
|
11
|
+
import initCommand from "./commands/init";
|
|
12
|
+
import logCommand from "./commands/log";
|
|
13
|
+
import modelsCommand from "./commands/models";
|
|
14
|
+
import newCommand from "./commands/new";
|
|
15
|
+
import pageCommand from "./commands/page";
|
|
16
|
+
import quitCommand from "./commands/quit";
|
|
17
|
+
import runCommand from "./commands/run";
|
|
18
|
+
import sessionsCommand from "./commands/sessions";
|
|
19
|
+
import undoCommand from "./commands/undo";
|
|
20
|
+
import { getLogDir, isLoggingEnabled } from "./logs";
|
|
21
|
+
import { startAnimation, stopAnimation, writePrompt } from "./render";
|
|
22
|
+
import { sendMessage } from "./server";
|
|
23
|
+
import type { State } from "./types";
|
|
24
|
+
|
|
25
|
+
const SLASH_COMMANDS = [
|
|
26
|
+
initCommand,
|
|
27
|
+
agentsCommand,
|
|
28
|
+
modelsCommand,
|
|
29
|
+
sessionsCommand,
|
|
30
|
+
newCommand,
|
|
31
|
+
undoCommand,
|
|
32
|
+
detailsCommand,
|
|
33
|
+
diffCommand,
|
|
34
|
+
debugCommand,
|
|
35
|
+
logCommand,
|
|
36
|
+
pageCommand,
|
|
37
|
+
exitCommand,
|
|
38
|
+
quitCommand,
|
|
39
|
+
runCommand,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
let inputBuffer = "";
|
|
43
|
+
let cursorPosition = 0;
|
|
44
|
+
let completions: string[] = [];
|
|
45
|
+
let history: string[] = [];
|
|
46
|
+
let historyIndex = history.length;
|
|
47
|
+
let selectedCompletion = 0;
|
|
48
|
+
let completionCycling = false;
|
|
49
|
+
let lastSpaceTime = 0;
|
|
50
|
+
let currentInputBuffer: string | null = null;
|
|
51
|
+
let isRequestActive = false;
|
|
52
|
+
|
|
53
|
+
let oldInputBuffer = "";
|
|
54
|
+
let oldWrappedRows = 0;
|
|
55
|
+
let oldCursorRow = 0;
|
|
56
|
+
export function renderLine(): void {
|
|
57
|
+
const consoleWidth = process.stdout.columns || 80;
|
|
58
|
+
|
|
59
|
+
// Move to the start of the line (i.e. the prompt position)
|
|
60
|
+
readline.cursorTo(process.stdout, 0);
|
|
61
|
+
if (oldWrappedRows > 0) {
|
|
62
|
+
if (cursorPosition < inputBuffer.length) {
|
|
63
|
+
readline.moveCursor(process.stdout, 0, oldWrappedRows - oldCursorRow);
|
|
64
|
+
}
|
|
65
|
+
readline.moveCursor(process.stdout, 0, -oldWrappedRows);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Find the position where the input has changed (i.e. where the user has
|
|
69
|
+
// typed something)
|
|
70
|
+
let start = 0;
|
|
71
|
+
let currentCol = 2;
|
|
72
|
+
let newWrappedRows = 0;
|
|
73
|
+
for (let i = 0; i < Math.min(oldInputBuffer.length, inputBuffer.length); i++) {
|
|
74
|
+
if (oldInputBuffer[i] !== inputBuffer[i]) {
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
if (currentCol >= consoleWidth) {
|
|
78
|
+
readline.moveCursor(process.stdout, 0, 1);
|
|
79
|
+
currentCol = 0;
|
|
80
|
+
newWrappedRows++;
|
|
81
|
+
}
|
|
82
|
+
currentCol++;
|
|
83
|
+
start++;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Clear the old, changed, input
|
|
87
|
+
readline.cursorTo(process.stdout, currentCol);
|
|
88
|
+
readline.clearScreenDown(process.stdout);
|
|
89
|
+
|
|
90
|
+
// Write the prompt if this is a fresh buffer
|
|
91
|
+
if (start === 0) {
|
|
92
|
+
readline.cursorTo(process.stdout, 0);
|
|
93
|
+
writePrompt();
|
|
94
|
+
readline.cursorTo(process.stdout, 2);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Write the changes from the new input buffer
|
|
98
|
+
let renderExtent = Math.max(cursorPosition + 1, inputBuffer.length);
|
|
99
|
+
for (let i = start; i < renderExtent; i++) {
|
|
100
|
+
if (currentCol >= consoleWidth) {
|
|
101
|
+
process.stdout.write("\n");
|
|
102
|
+
currentCol = 0;
|
|
103
|
+
newWrappedRows++;
|
|
104
|
+
}
|
|
105
|
+
if (i < inputBuffer.length) {
|
|
106
|
+
process.stdout.write(inputBuffer[i]!);
|
|
107
|
+
}
|
|
108
|
+
currentCol++;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Calculate and move to the cursor's position
|
|
112
|
+
let absolutePos = 2 + cursorPosition;
|
|
113
|
+
let newCursorRow = Math.floor(absolutePos / consoleWidth);
|
|
114
|
+
let newCursorCol = absolutePos % consoleWidth;
|
|
115
|
+
readline.cursorTo(process.stdout, 0);
|
|
116
|
+
readline.moveCursor(process.stdout, 0, -1 * (newWrappedRows - newCursorRow));
|
|
117
|
+
readline.cursorTo(process.stdout, newCursorCol);
|
|
118
|
+
|
|
119
|
+
oldInputBuffer = inputBuffer;
|
|
120
|
+
oldWrappedRows = newWrappedRows;
|
|
121
|
+
oldCursorRow = newCursorRow;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function handleKeyPress(state: State, str: string, key: Key) {
|
|
125
|
+
if (key.ctrl && key.name === "c") {
|
|
126
|
+
process.stdout.write("\n");
|
|
127
|
+
state.shutdown();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (let command of SLASH_COMMANDS) {
|
|
132
|
+
if (command.running && command.handleKey) {
|
|
133
|
+
await command.handleKey(state, key, str);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
switch (key.name) {
|
|
139
|
+
case "up": {
|
|
140
|
+
if (historyIndex === history.length) {
|
|
141
|
+
currentInputBuffer = inputBuffer;
|
|
142
|
+
}
|
|
143
|
+
if (history.length > 0) {
|
|
144
|
+
if (historyIndex > 0) {
|
|
145
|
+
historyIndex--;
|
|
146
|
+
inputBuffer = history[historyIndex]!;
|
|
147
|
+
} else {
|
|
148
|
+
historyIndex = Math.max(-1, historyIndex - 1);
|
|
149
|
+
inputBuffer = "";
|
|
150
|
+
}
|
|
151
|
+
cursorPosition = inputBuffer.length;
|
|
152
|
+
renderLine();
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
case "down": {
|
|
157
|
+
if (history.length > 0) {
|
|
158
|
+
if (historyIndex < history.length - 1) {
|
|
159
|
+
historyIndex++;
|
|
160
|
+
inputBuffer = history[historyIndex]!;
|
|
161
|
+
} else {
|
|
162
|
+
historyIndex = history.length;
|
|
163
|
+
inputBuffer = currentInputBuffer || "";
|
|
164
|
+
currentInputBuffer = null;
|
|
165
|
+
}
|
|
166
|
+
cursorPosition = inputBuffer.length;
|
|
167
|
+
renderLine();
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
case "tab": {
|
|
172
|
+
if (!completionCycling) {
|
|
173
|
+
await handleTab();
|
|
174
|
+
}
|
|
175
|
+
if (completionCycling && completions.length > 0) {
|
|
176
|
+
await handleTab();
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
case "escape": {
|
|
181
|
+
if (isRequestActive) {
|
|
182
|
+
if (state.sessionID) {
|
|
183
|
+
state.client.session.abort({ path: { id: state.sessionID } }).catch(() => {});
|
|
184
|
+
}
|
|
185
|
+
stopAnimation();
|
|
186
|
+
process.stdout.write(ansi.CURSOR_SHOW);
|
|
187
|
+
process.stdout.write(`\r ${ansi.BRIGHT_BLACK}Cancelled request${ansi.RESET}\n`);
|
|
188
|
+
writePrompt();
|
|
189
|
+
isRequestActive = false;
|
|
190
|
+
} else {
|
|
191
|
+
inputBuffer = "";
|
|
192
|
+
cursorPosition = 0;
|
|
193
|
+
currentInputBuffer = null;
|
|
194
|
+
renderLine();
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
case "return": {
|
|
199
|
+
await acceptInput(state);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
case "backspace": {
|
|
203
|
+
if (cursorPosition > 0) {
|
|
204
|
+
inputBuffer = inputBuffer.slice(0, cursorPosition - 1) + inputBuffer.slice(cursorPosition);
|
|
205
|
+
cursorPosition--;
|
|
206
|
+
currentInputBuffer = null;
|
|
207
|
+
}
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
case "delete": {
|
|
211
|
+
if (cursorPosition < inputBuffer.length) {
|
|
212
|
+
inputBuffer = inputBuffer.slice(0, cursorPosition) + inputBuffer.slice(cursorPosition + 1);
|
|
213
|
+
currentInputBuffer = null;
|
|
214
|
+
}
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
case "left": {
|
|
218
|
+
if (key.meta) {
|
|
219
|
+
cursorPosition = findPreviousWordBoundary(inputBuffer, cursorPosition);
|
|
220
|
+
} else if (cursorPosition > 0) {
|
|
221
|
+
cursorPosition--;
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
case "right": {
|
|
226
|
+
if (key.meta) {
|
|
227
|
+
cursorPosition = findNextWordBoundary(inputBuffer, cursorPosition);
|
|
228
|
+
} else if (cursorPosition < inputBuffer.length) {
|
|
229
|
+
cursorPosition++;
|
|
230
|
+
}
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
default: {
|
|
234
|
+
if (str === " ") {
|
|
235
|
+
const now = Date.now();
|
|
236
|
+
if (
|
|
237
|
+
now - lastSpaceTime < 500 &&
|
|
238
|
+
cursorPosition > 0 &&
|
|
239
|
+
inputBuffer[cursorPosition - 1] === " "
|
|
240
|
+
) {
|
|
241
|
+
inputBuffer =
|
|
242
|
+
inputBuffer.slice(0, cursorPosition - 1) + ". " + inputBuffer.slice(cursorPosition);
|
|
243
|
+
cursorPosition += 1;
|
|
244
|
+
} else {
|
|
245
|
+
inputBuffer =
|
|
246
|
+
inputBuffer.slice(0, cursorPosition) + str + inputBuffer.slice(cursorPosition);
|
|
247
|
+
cursorPosition += str.length;
|
|
248
|
+
}
|
|
249
|
+
lastSpaceTime = now;
|
|
250
|
+
} else if (str) {
|
|
251
|
+
inputBuffer =
|
|
252
|
+
inputBuffer.slice(0, cursorPosition) + str + inputBuffer.slice(cursorPosition);
|
|
253
|
+
cursorPosition += str.length;
|
|
254
|
+
}
|
|
255
|
+
currentInputBuffer = null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
completionCycling = false;
|
|
260
|
+
completions = [];
|
|
261
|
+
renderLine();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function handleTab(): Promise<void> {
|
|
265
|
+
const potentialCompletions = await getCompletions(inputBuffer);
|
|
266
|
+
|
|
267
|
+
if (potentialCompletions.length === 0) {
|
|
268
|
+
completionCycling = false;
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!completionCycling) {
|
|
273
|
+
completions = potentialCompletions;
|
|
274
|
+
selectedCompletion = 0;
|
|
275
|
+
completionCycling = true;
|
|
276
|
+
inputBuffer = completions[0]!;
|
|
277
|
+
cursorPosition = inputBuffer.length;
|
|
278
|
+
renderLine();
|
|
279
|
+
} else {
|
|
280
|
+
selectedCompletion = (selectedCompletion + 1) % completions.length;
|
|
281
|
+
inputBuffer = completions[selectedCompletion]!;
|
|
282
|
+
cursorPosition = inputBuffer.length;
|
|
283
|
+
renderLine();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function getCompletions(text: string): Promise<string[]> {
|
|
288
|
+
if (text.startsWith("/")) {
|
|
289
|
+
return ["/help", ...SLASH_COMMANDS.map((c) => c.name)].filter((cmd) => cmd.startsWith(text));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const atMatch = text.match(/(@[^\s]*)$/);
|
|
293
|
+
if (atMatch) {
|
|
294
|
+
const prefix = atMatch[0]!;
|
|
295
|
+
const searchPattern = prefix.slice(1);
|
|
296
|
+
const pattern = searchPattern.includes("/") ? searchPattern + "*" : "**/" + searchPattern + "*";
|
|
297
|
+
const files = await getFileCompletions(pattern);
|
|
298
|
+
return files.map((file: string) => text.replace(/@[^\s]*$/, "@" + file));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function getFileCompletions(pattern: string): Promise<string[]> {
|
|
305
|
+
try {
|
|
306
|
+
const files: string[] = [];
|
|
307
|
+
for await (const file of glob(pattern)) {
|
|
308
|
+
if (
|
|
309
|
+
!file.startsWith("node_modules/") &&
|
|
310
|
+
!file.startsWith(".git/") &&
|
|
311
|
+
!file.startsWith("dist/") &&
|
|
312
|
+
!file.startsWith("build/")
|
|
313
|
+
) {
|
|
314
|
+
const isDir = await stat(file)
|
|
315
|
+
.then((s) => s.isDirectory())
|
|
316
|
+
.catch(() => false);
|
|
317
|
+
files.push(isDir ? file + "/" : file);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return files.sort();
|
|
321
|
+
} catch {
|
|
322
|
+
return [];
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function acceptInput(state: State): Promise<void> {
|
|
327
|
+
process.stdout.write("\n");
|
|
328
|
+
|
|
329
|
+
const input = inputBuffer.trim();
|
|
330
|
+
|
|
331
|
+
oldInputBuffer = "";
|
|
332
|
+
oldWrappedRows = 0;
|
|
333
|
+
oldCursorRow = 0;
|
|
334
|
+
|
|
335
|
+
inputBuffer = "";
|
|
336
|
+
cursorPosition = 0;
|
|
337
|
+
completionCycling = false;
|
|
338
|
+
completions = [];
|
|
339
|
+
currentInputBuffer = null;
|
|
340
|
+
|
|
341
|
+
if (input) {
|
|
342
|
+
if (history[history.length - 1] !== input) {
|
|
343
|
+
history.push(input);
|
|
344
|
+
}
|
|
345
|
+
historyIndex = history.length;
|
|
346
|
+
try {
|
|
347
|
+
if (input === "/help") {
|
|
348
|
+
process.stdout.write("\n");
|
|
349
|
+
const maxCommandLength = Math.max(...SLASH_COMMANDS.map((c) => c.name.length));
|
|
350
|
+
for (const cmd of SLASH_COMMANDS) {
|
|
351
|
+
const padding = " ".repeat(maxCommandLength - cmd.name.length + 2);
|
|
352
|
+
console.log(
|
|
353
|
+
` ${ansi.BRIGHT_WHITE}${cmd.name}${ansi.RESET}${padding}${ansi.BRIGHT_BLACK}${cmd.description}${ansi.RESET}`,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
console.log();
|
|
357
|
+
writePrompt();
|
|
358
|
+
return;
|
|
359
|
+
} else if (input.startsWith("/")) {
|
|
360
|
+
const parts = input.match(/(\/[^\s]+)\s*(.*)/)!;
|
|
361
|
+
if (parts) {
|
|
362
|
+
const commandName = parts[1];
|
|
363
|
+
const extra = parts[2]?.trim();
|
|
364
|
+
for (let command of SLASH_COMMANDS) {
|
|
365
|
+
if (command.name === commandName) {
|
|
366
|
+
process.stdout.write("\n");
|
|
367
|
+
await command.run(state, extra);
|
|
368
|
+
writePrompt();
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
isRequestActive = true;
|
|
377
|
+
process.stdout.write("\n");
|
|
378
|
+
process.stdout.write(ansi.CURSOR_HIDE);
|
|
379
|
+
startAnimation();
|
|
380
|
+
if (isLoggingEnabled()) {
|
|
381
|
+
console.log(`📝 ${ansi.BRIGHT_BLACK}Logging to ${getLogDir()}\n${ansi.RESET}`);
|
|
382
|
+
}
|
|
383
|
+
await sendMessage(state, input);
|
|
384
|
+
isRequestActive = false;
|
|
385
|
+
} catch (error: any) {
|
|
386
|
+
isRequestActive = false;
|
|
387
|
+
if (error.message !== "Request cancelled") {
|
|
388
|
+
stopAnimation();
|
|
389
|
+
console.error("Error:", error.message);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export async function loadSessionHistory(state: State): Promise<string[]> {
|
|
396
|
+
try {
|
|
397
|
+
const result = await state.client.session.messages({
|
|
398
|
+
path: { id: state.sessionID },
|
|
399
|
+
});
|
|
400
|
+
if (result.error || !result.data) {
|
|
401
|
+
return [];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const history: string[] = [];
|
|
405
|
+
for (const msg of result.data) {
|
|
406
|
+
if (msg.info.role === "user") {
|
|
407
|
+
const textParts = msg.parts
|
|
408
|
+
.filter((p: Part) => p.type === "text")
|
|
409
|
+
.map((p: Part) => (p as any).text || "")
|
|
410
|
+
.filter(Boolean);
|
|
411
|
+
const text = textParts.join("").trim();
|
|
412
|
+
if (text && !text.startsWith("/")) {
|
|
413
|
+
history.push(text);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return history;
|
|
418
|
+
} catch {
|
|
419
|
+
return [];
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function findPreviousWordBoundary(text: string, pos: number): number {
|
|
424
|
+
if (pos <= 0) return 0;
|
|
425
|
+
|
|
426
|
+
let newPos = pos;
|
|
427
|
+
|
|
428
|
+
while (newPos > 0 && /\s/.test(text[newPos - 1]!)) {
|
|
429
|
+
newPos--;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
while (newPos > 0 && !/\s/.test(text[newPos - 1]!)) {
|
|
433
|
+
newPos--;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return newPos;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function findNextWordBoundary(text: string, pos: number): number {
|
|
440
|
+
if (pos >= text.length) return text.length;
|
|
441
|
+
|
|
442
|
+
let newPos = pos;
|
|
443
|
+
|
|
444
|
+
while (newPos < text.length && !/\s/.test(text[newPos]!)) {
|
|
445
|
+
newPos++;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
while (newPos < text.length && /\s/.test(text[newPos]!)) {
|
|
449
|
+
newPos++;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return newPos;
|
|
453
|
+
}
|
package/src/logs.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { open } from "node:fs/promises";
|
|
3
|
+
import { config } from "./config";
|
|
4
|
+
|
|
5
|
+
let logFile: Awaited<ReturnType<typeof open>> | null = null;
|
|
6
|
+
let logFilePath: string | null = null;
|
|
7
|
+
|
|
8
|
+
export function isLoggingEnabled(): boolean {
|
|
9
|
+
return config.loggingEnabled;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getLogDir(): string {
|
|
13
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
14
|
+
return `${homeDir}/.local/share/opencode-miniterm/log`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function createLogFile(): Promise<void> {
|
|
18
|
+
if (!isLoggingEnabled()) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const logDir = getLogDir();
|
|
23
|
+
await mkdir(logDir, { recursive: true });
|
|
24
|
+
|
|
25
|
+
const now = new Date();
|
|
26
|
+
const timestamp = now.toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
27
|
+
const filename = `${timestamp}.txt`;
|
|
28
|
+
logFilePath = `${logDir}/${filename}`;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
logFile = await open(logFilePath, "w");
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error("Failed to create log file:", error);
|
|
34
|
+
logFile = null;
|
|
35
|
+
logFilePath = null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function closeLogFile(): Promise<void> {
|
|
40
|
+
if (logFile) {
|
|
41
|
+
try {
|
|
42
|
+
await logFile.close();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error("Failed to close log file:", error);
|
|
45
|
+
}
|
|
46
|
+
logFile = null;
|
|
47
|
+
logFilePath = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function writeToLog(text: string): Promise<void> {
|
|
52
|
+
if (logFile && isLoggingEnabled()) {
|
|
53
|
+
try {
|
|
54
|
+
await logFile.write(text);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error("Failed to write to log file:", error);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/render.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { OpencodeClient } from "@opencode-ai/sdk";
|
|
2
2
|
import { gfm, parse, renderToConsole } from "allmark";
|
|
3
|
-
import readline from "node:readline";
|
|
4
3
|
import * as ansi from "./ansi";
|
|
5
4
|
import { config } from "./config";
|
|
6
|
-
import type { State } from "./
|
|
5
|
+
import type { State } from "./types";
|
|
6
|
+
import { formatDuration } from "./utils";
|
|
7
7
|
|
|
8
8
|
export function render(state: State, details = false): void {
|
|
9
9
|
let output = "";
|
|
@@ -139,10 +139,9 @@ function lastThinkingLines(text: string): string {
|
|
|
139
139
|
|
|
140
140
|
function clearRenderedLines(state: State, linesToClear: number): void {
|
|
141
141
|
if (linesToClear > 0) {
|
|
142
|
-
|
|
143
|
-
readline.clearScreenDown(process.stdout);
|
|
142
|
+
state.write(`${ansi.CURSOR_UP(linesToClear)}${ansi.CLEAR_FROM_CURSOR}`);
|
|
144
143
|
}
|
|
145
|
-
|
|
144
|
+
state.write(`${ansi.CURSOR_HOME}`);
|
|
146
145
|
}
|
|
147
146
|
|
|
148
147
|
export function wrapText(text: string, width: number): string[] {
|
|
@@ -309,21 +308,6 @@ export function startAnimation(startTime?: number): void {
|
|
|
309
308
|
}, 100);
|
|
310
309
|
}
|
|
311
310
|
|
|
312
|
-
function formatDuration(ms: number): string {
|
|
313
|
-
const seconds = ms / 1000;
|
|
314
|
-
if (seconds < 60) {
|
|
315
|
-
return `${Math.round(seconds)}s`;
|
|
316
|
-
}
|
|
317
|
-
const minutes = Math.floor(seconds / 60);
|
|
318
|
-
const remainingSeconds = Math.round(seconds % 60);
|
|
319
|
-
if (minutes < 60) {
|
|
320
|
-
return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
|
|
321
|
-
}
|
|
322
|
-
const hours = Math.floor(minutes / 60);
|
|
323
|
-
const remainingMinutes = minutes % 60;
|
|
324
|
-
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
311
|
export function stopAnimation(): void {
|
|
328
312
|
if (animationInterval) {
|
|
329
313
|
clearInterval(animationInterval);
|
|
@@ -378,3 +362,22 @@ export async function getActiveDisplay(client: OpencodeClient): Promise<string>
|
|
|
378
362
|
|
|
379
363
|
return parts.join(" ");
|
|
380
364
|
}
|
|
365
|
+
|
|
366
|
+
export async function updateSessionTitle(state: State): Promise<void> {
|
|
367
|
+
try {
|
|
368
|
+
const result = await state.client.session.get({
|
|
369
|
+
path: { id: state.sessionID },
|
|
370
|
+
});
|
|
371
|
+
if (!result.error && result.data?.title) {
|
|
372
|
+
setTerminalTitle(result.data.title);
|
|
373
|
+
} else {
|
|
374
|
+
setTerminalTitle(state.sessionID.substring(0, 8));
|
|
375
|
+
}
|
|
376
|
+
} catch {
|
|
377
|
+
setTerminalTitle(state.sessionID.substring(0, 8));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function setTerminalTitle(sessionName: string): void {
|
|
382
|
+
process.stdout.write(`\x1b]0;OC | ${sessionName}\x07`);
|
|
383
|
+
}
|