opencode-miniterm 1.0.1 → 1.0.3
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/AGENTS.md +46 -11
- package/README.md +164 -1
- package/bun.lock +3 -3
- package/package.json +3 -3
- package/src/ansi.ts +4 -0
- package/src/commands/diff.ts +3 -2
- package/src/commands/init.ts +3 -2
- package/src/commands/new.ts +1 -1
- package/src/commands/page.ts +9 -6
- package/src/commands/sessions.ts +4 -4
- package/src/commands/undo.ts +4 -3
- package/src/config.ts +2 -1
- package/src/index.ts +108 -39
- package/src/render.ts +57 -35
- package/test/render.test.ts +54 -41
- package/src/commands/kill.ts +0 -33
package/src/index.ts
CHANGED
|
@@ -14,7 +14,6 @@ import detailsCommand from "./commands/details";
|
|
|
14
14
|
import diffCommand from "./commands/diff";
|
|
15
15
|
import exitCommand from "./commands/exit";
|
|
16
16
|
import initCommand from "./commands/init";
|
|
17
|
-
import killCommand from "./commands/kill";
|
|
18
17
|
import logCommand, { isLoggingEnabled } from "./commands/log";
|
|
19
18
|
import modelsCommand from "./commands/models";
|
|
20
19
|
import newCommand from "./commands/new";
|
|
@@ -42,7 +41,6 @@ const SLASH_COMMANDS = [
|
|
|
42
41
|
debugCommand,
|
|
43
42
|
logCommand,
|
|
44
43
|
pageCommand,
|
|
45
|
-
killCommand,
|
|
46
44
|
exitCommand,
|
|
47
45
|
quitCommand,
|
|
48
46
|
runCommand,
|
|
@@ -52,6 +50,7 @@ let client: ReturnType<typeof createOpencodeClient>;
|
|
|
52
50
|
|
|
53
51
|
let processing = true;
|
|
54
52
|
let retryInterval: ReturnType<typeof setInterval> | null = null;
|
|
53
|
+
let isRequestActive = false;
|
|
55
54
|
|
|
56
55
|
interface AccumulatedPart {
|
|
57
56
|
key: string;
|
|
@@ -122,11 +121,11 @@ async function main() {
|
|
|
122
121
|
try {
|
|
123
122
|
let isNewSession = false;
|
|
124
123
|
|
|
125
|
-
const initialSessionID = config.
|
|
124
|
+
const initialSessionID = config.sessionIDs[cwd];
|
|
126
125
|
if (!initialSessionID || !(await validateSession(initialSessionID))) {
|
|
127
126
|
state.sessionID = await createSession();
|
|
128
127
|
isNewSession = true;
|
|
129
|
-
config.
|
|
128
|
+
config.sessionIDs[cwd] = state.sessionID;
|
|
130
129
|
saveConfig();
|
|
131
130
|
} else {
|
|
132
131
|
state.sessionID = initialSessionID;
|
|
@@ -136,6 +135,8 @@ async function main() {
|
|
|
136
135
|
|
|
137
136
|
await updateSessionTitle();
|
|
138
137
|
|
|
138
|
+
const sessionHistory = await loadSessionHistory();
|
|
139
|
+
|
|
139
140
|
const activeDisplay = await getActiveDisplay(client);
|
|
140
141
|
|
|
141
142
|
process.stdout.write(`${ansi.CLEAR_SCREEN_UP}${ansi.CLEAR_FROM_CURSOR}`);
|
|
@@ -160,11 +161,13 @@ async function main() {
|
|
|
160
161
|
let inputBuffer = "";
|
|
161
162
|
let cursorPosition = 0;
|
|
162
163
|
let completions: string[] = [];
|
|
163
|
-
let history: string[] =
|
|
164
|
-
let historyIndex =
|
|
164
|
+
let history: string[] = sessionHistory;
|
|
165
|
+
let historyIndex = history.length;
|
|
165
166
|
let selectedCompletion = 0;
|
|
166
167
|
let showCompletions = false;
|
|
167
168
|
let completionCycling = false;
|
|
169
|
+
let lastSpaceTime = 0;
|
|
170
|
+
let currentInputBuffer: string | null = null;
|
|
168
171
|
|
|
169
172
|
const getCompletions = async (text: string): Promise<string[]> => {
|
|
170
173
|
if (text.startsWith("/")) {
|
|
@@ -251,6 +254,7 @@ async function main() {
|
|
|
251
254
|
showCompletions = false;
|
|
252
255
|
completionCycling = false;
|
|
253
256
|
completions = [];
|
|
257
|
+
currentInputBuffer = null;
|
|
254
258
|
|
|
255
259
|
if (input) {
|
|
256
260
|
if (history[history.length - 1] !== input) {
|
|
@@ -283,13 +287,16 @@ async function main() {
|
|
|
283
287
|
return;
|
|
284
288
|
}
|
|
285
289
|
|
|
290
|
+
isRequestActive = true;
|
|
286
291
|
process.stdout.write(ansi.CURSOR_HIDE);
|
|
287
292
|
startAnimation();
|
|
288
293
|
if (isLoggingEnabled()) {
|
|
289
294
|
console.log(`📝 ${ansi.BRIGHT_BLACK}Logging to ${getLogDir()}\n${ansi.RESET}`);
|
|
290
295
|
}
|
|
291
296
|
await sendMessage(state.sessionID, input);
|
|
297
|
+
isRequestActive = false;
|
|
292
298
|
} catch (error: any) {
|
|
299
|
+
isRequestActive = false;
|
|
293
300
|
if (error.message !== "Request cancelled") {
|
|
294
301
|
stopAnimation();
|
|
295
302
|
console.error("Error:", error.message);
|
|
@@ -312,11 +319,17 @@ async function main() {
|
|
|
312
319
|
|
|
313
320
|
switch (key.name) {
|
|
314
321
|
case "up": {
|
|
322
|
+
if (historyIndex === history.length) {
|
|
323
|
+
currentInputBuffer = inputBuffer;
|
|
324
|
+
}
|
|
315
325
|
if (history.length > 0) {
|
|
316
326
|
if (historyIndex > 0) {
|
|
317
327
|
historyIndex--;
|
|
328
|
+
inputBuffer = history[historyIndex]!;
|
|
329
|
+
} else {
|
|
330
|
+
historyIndex = Math.max(-1, historyIndex - 1);
|
|
331
|
+
inputBuffer = "";
|
|
318
332
|
}
|
|
319
|
-
inputBuffer = history[historyIndex]!;
|
|
320
333
|
cursorPosition = inputBuffer.length;
|
|
321
334
|
renderLine();
|
|
322
335
|
}
|
|
@@ -326,9 +339,11 @@ async function main() {
|
|
|
326
339
|
if (history.length > 0) {
|
|
327
340
|
if (historyIndex < history.length - 1) {
|
|
328
341
|
historyIndex++;
|
|
342
|
+
inputBuffer = history[historyIndex]!;
|
|
329
343
|
} else {
|
|
330
344
|
historyIndex = history.length;
|
|
331
|
-
inputBuffer = "";
|
|
345
|
+
inputBuffer = currentInputBuffer || "";
|
|
346
|
+
currentInputBuffer = null;
|
|
332
347
|
}
|
|
333
348
|
cursorPosition = inputBuffer.length;
|
|
334
349
|
renderLine();
|
|
@@ -345,13 +360,21 @@ async function main() {
|
|
|
345
360
|
return;
|
|
346
361
|
}
|
|
347
362
|
case "escape": {
|
|
348
|
-
if (
|
|
349
|
-
|
|
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();
|
|
350
377
|
}
|
|
351
|
-
stopAnimation();
|
|
352
|
-
process.stdout.write(ansi.CURSOR_SHOW);
|
|
353
|
-
process.stdout.write(`\r${ansi.BRIGHT_BLACK}Cancelled request${ansi.RESET}\n`);
|
|
354
|
-
writePrompt();
|
|
355
378
|
return;
|
|
356
379
|
}
|
|
357
380
|
case "return": {
|
|
@@ -363,6 +386,7 @@ async function main() {
|
|
|
363
386
|
inputBuffer =
|
|
364
387
|
inputBuffer.slice(0, cursorPosition - 1) + inputBuffer.slice(cursorPosition);
|
|
365
388
|
cursorPosition--;
|
|
389
|
+
currentInputBuffer = null;
|
|
366
390
|
}
|
|
367
391
|
break;
|
|
368
392
|
}
|
|
@@ -370,6 +394,7 @@ async function main() {
|
|
|
370
394
|
if (cursorPosition < inputBuffer.length) {
|
|
371
395
|
inputBuffer =
|
|
372
396
|
inputBuffer.slice(0, cursorPosition) + inputBuffer.slice(cursorPosition + 1);
|
|
397
|
+
currentInputBuffer = null;
|
|
373
398
|
}
|
|
374
399
|
break;
|
|
375
400
|
}
|
|
@@ -391,9 +416,30 @@ async function main() {
|
|
|
391
416
|
}
|
|
392
417
|
default: {
|
|
393
418
|
if (str) {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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;
|
|
441
|
+
}
|
|
442
|
+
currentInputBuffer = null;
|
|
397
443
|
}
|
|
398
444
|
}
|
|
399
445
|
}
|
|
@@ -461,6 +507,34 @@ async function updateSessionTitle(): Promise<void> {
|
|
|
461
507
|
}
|
|
462
508
|
}
|
|
463
509
|
|
|
510
|
+
async function loadSessionHistory(): Promise<string[]> {
|
|
511
|
+
try {
|
|
512
|
+
const result = await client.session.messages({
|
|
513
|
+
path: { id: state.sessionID },
|
|
514
|
+
});
|
|
515
|
+
if (result.error || !result.data) {
|
|
516
|
+
return [];
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const history: string[] = [];
|
|
520
|
+
for (const msg of result.data) {
|
|
521
|
+
if (msg.info.role === "user") {
|
|
522
|
+
const textParts = msg.parts
|
|
523
|
+
.filter((p: Part) => p.type === "text")
|
|
524
|
+
.map((p: Part) => (p as any).text || "")
|
|
525
|
+
.filter(Boolean);
|
|
526
|
+
const text = textParts.join("").trim();
|
|
527
|
+
if (text && !text.startsWith("/")) {
|
|
528
|
+
history.push(text);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return history;
|
|
533
|
+
} catch {
|
|
534
|
+
return [];
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
464
538
|
async function startEventListener(): Promise<void> {
|
|
465
539
|
try {
|
|
466
540
|
const { stream } = await client.event.subscribe({
|
|
@@ -527,7 +601,7 @@ async function sendMessage(sessionID: string, message: string) {
|
|
|
527
601
|
|
|
528
602
|
const duration = Date.now() - requestStartTime;
|
|
529
603
|
const durationText = formatDuration(duration);
|
|
530
|
-
console.log(
|
|
604
|
+
console.log(` ${ansi.BRIGHT_BLACK}Completed in ${durationText}${ansi.RESET}\n`);
|
|
531
605
|
|
|
532
606
|
writePrompt();
|
|
533
607
|
|
|
@@ -591,6 +665,7 @@ async function processEvent(event: Event): Promise<void> {
|
|
|
591
665
|
case "session.status":
|
|
592
666
|
if (event.type === "session.status" && event.properties.status.type === "idle") {
|
|
593
667
|
stopAnimation();
|
|
668
|
+
isRequestActive = false;
|
|
594
669
|
process.stdout.write(ansi.CURSOR_SHOW);
|
|
595
670
|
if (retryInterval) {
|
|
596
671
|
clearInterval(retryInterval);
|
|
@@ -699,7 +774,7 @@ async function processReasoning(part: Part) {
|
|
|
699
774
|
|
|
700
775
|
const text = (part as any).text || "";
|
|
701
776
|
const cleanText = ansi.stripAnsiCodes(text.trimStart());
|
|
702
|
-
await writeToLog(
|
|
777
|
+
await writeToLog(`Thinking:\n\n${cleanText}\n\n`);
|
|
703
778
|
|
|
704
779
|
render(state);
|
|
705
780
|
}
|
|
@@ -715,7 +790,7 @@ async function processText(part: Part) {
|
|
|
715
790
|
|
|
716
791
|
const text = (part as any).text || "";
|
|
717
792
|
const cleanText = ansi.stripAnsiCodes(text.trimStart());
|
|
718
|
-
await writeToLog(
|
|
793
|
+
await writeToLog(`Response:\n\n${cleanText}\n\n`);
|
|
719
794
|
|
|
720
795
|
render(state);
|
|
721
796
|
}
|
|
@@ -724,7 +799,7 @@ async function processToolUse(part: Part) {
|
|
|
724
799
|
const toolPart = part as ToolPart;
|
|
725
800
|
const toolName = toolPart.tool || "unknown";
|
|
726
801
|
const toolInput = toolPart.state.input["description"] || toolPart.state.input["filePath"] || {};
|
|
727
|
-
const toolText =
|
|
802
|
+
const toolText = `${ansi.BRIGHT_BLACK}$${ansi.RESET} ${toolName}: ${ansi.BRIGHT_BLACK}${toolInput}${ansi.RESET}`;
|
|
728
803
|
|
|
729
804
|
if (state.accumulatedResponse[state.accumulatedResponse.length - 1]?.title === "tool") {
|
|
730
805
|
state.accumulatedResponse[state.accumulatedResponse.length - 1]!.text = toolText;
|
|
@@ -733,7 +808,7 @@ async function processToolUse(part: Part) {
|
|
|
733
808
|
}
|
|
734
809
|
|
|
735
810
|
const cleanToolText = ansi.stripAnsiCodes(toolText);
|
|
736
|
-
await writeToLog(`${cleanToolText}\n\n`);
|
|
811
|
+
await writeToLog(`$ ${cleanToolText}\n\n`);
|
|
737
812
|
|
|
738
813
|
render(state);
|
|
739
814
|
}
|
|
@@ -748,28 +823,26 @@ function processDelta(partID: string, delta: string) {
|
|
|
748
823
|
}
|
|
749
824
|
|
|
750
825
|
async function processDiff(diff: FileDiff[]) {
|
|
751
|
-
let hasChanges = false;
|
|
752
826
|
const parts: string[] = [];
|
|
753
827
|
for (const file of diff) {
|
|
754
|
-
const status = !file.before ? "added" : !file.after ? "deleted" : "modified";
|
|
755
|
-
const statusIcon = status === "added" ? "A" : status === "modified" ? "M" : "D";
|
|
756
|
-
const statusLabel =
|
|
757
|
-
status === "added" ? "added" : status === "modified" ? "modified" : "deleted";
|
|
758
|
-
const addStr = file.additions > 0 ? `${ansi.GREEN}+${file.additions}${ansi.RESET}` : "";
|
|
759
|
-
const delStr = file.deletions > 0 ? `${ansi.RED}-${file.deletions}${ansi.RESET}` : "";
|
|
760
|
-
const stats = [addStr, delStr].filter(Boolean).join(" ");
|
|
761
|
-
const line = ` ${ansi.BLUE}${statusIcon}${ansi.RESET} ${file.file} (${statusLabel}) ${stats}`;
|
|
762
|
-
parts.push(line);
|
|
763
|
-
|
|
764
828
|
const newAfter = file.after ?? "";
|
|
765
829
|
const oldAfter = state.lastFileAfter.get(file.file);
|
|
766
830
|
if (newAfter !== oldAfter) {
|
|
767
|
-
|
|
831
|
+
const status = !file.before ? "added" : !file.after ? "deleted" : "modified";
|
|
832
|
+
const statusIcon = status === "added" ? "A" : status === "modified" ? "M" : "D";
|
|
833
|
+
const statusLabel =
|
|
834
|
+
status === "added" ? "added" : status === "modified" ? "modified" : "deleted";
|
|
835
|
+
const addStr = file.additions > 0 ? `${ansi.GREEN}+${file.additions}${ansi.RESET}` : "";
|
|
836
|
+
const delStr = file.deletions > 0 ? `${ansi.RED}-${file.deletions}${ansi.RESET}` : "";
|
|
837
|
+
const stats = [addStr, delStr].filter(Boolean).join(" ");
|
|
838
|
+
const line = `${ansi.BLUE}${statusIcon}${ansi.RESET} ${file.file} (${statusLabel}) ${stats}`;
|
|
839
|
+
parts.push(line);
|
|
840
|
+
|
|
768
841
|
state.lastFileAfter.set(file.file, newAfter);
|
|
769
842
|
}
|
|
770
843
|
}
|
|
771
844
|
|
|
772
|
-
if (
|
|
845
|
+
if (parts.length > 0) {
|
|
773
846
|
state.accumulatedResponse.push({ key: "diff", title: "files", text: parts.join("\n") });
|
|
774
847
|
|
|
775
848
|
const diffText = ansi.stripAnsiCodes(parts.join("\n"));
|
|
@@ -785,15 +858,11 @@ async function processTodos(todos: Todo[]) {
|
|
|
785
858
|
for (let todo of todos) {
|
|
786
859
|
let todoText = "";
|
|
787
860
|
if (todo.status === "completed") {
|
|
788
|
-
todoText += ansi.STRIKETHROUGH;
|
|
789
861
|
todoText += "- [✓] ";
|
|
790
862
|
} else {
|
|
791
863
|
todoText += "- [ ] ";
|
|
792
864
|
}
|
|
793
865
|
todoText += todo.content;
|
|
794
|
-
if (todo.status === "completed") {
|
|
795
|
-
todoText += ansi.RESET;
|
|
796
|
-
}
|
|
797
866
|
todoListText += todoText + "\n";
|
|
798
867
|
}
|
|
799
868
|
|
package/src/render.ts
CHANGED
|
@@ -12,11 +12,7 @@ export function render(state: State, details = false): void {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
// Only show the last (i.e. active) thinking part
|
|
15
|
-
// Only show the last (i.e. active) tool use
|
|
16
|
-
// Only show the last files part between parts
|
|
17
15
|
let foundPart = false;
|
|
18
|
-
let foundFiles = false;
|
|
19
|
-
let foundTodo = false;
|
|
20
16
|
for (let i = state.accumulatedResponse.length - 1; i >= 0; i--) {
|
|
21
17
|
const part = state.accumulatedResponse[i];
|
|
22
18
|
if (!part) continue;
|
|
@@ -26,18 +22,15 @@ export function render(state: State, details = false): void {
|
|
|
26
22
|
}
|
|
27
23
|
|
|
28
24
|
if (part.title === "thinking") {
|
|
25
|
+
if (part.active === false) {
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
29
28
|
part.active = !foundPart;
|
|
30
29
|
foundPart = true;
|
|
31
|
-
} else if (part.title === "
|
|
32
|
-
part.active =
|
|
33
|
-
} else if (part.title === "files") {
|
|
34
|
-
part.active = !foundFiles;
|
|
35
|
-
foundFiles = true;
|
|
36
|
-
} else if (part.title === "todo") {
|
|
37
|
-
part.active = !foundTodo;
|
|
38
|
-
foundTodo = true;
|
|
39
|
-
} else {
|
|
30
|
+
} else if (part.title === "response") {
|
|
31
|
+
part.active = true;
|
|
40
32
|
foundPart = true;
|
|
33
|
+
} else {
|
|
41
34
|
part.active = true;
|
|
42
35
|
}
|
|
43
36
|
}
|
|
@@ -48,17 +41,19 @@ export function render(state: State, details = false): void {
|
|
|
48
41
|
if (!part.text.trim()) continue;
|
|
49
42
|
|
|
50
43
|
if (part.title === "thinking") {
|
|
44
|
+
// Show max 10 thinking lines
|
|
51
45
|
const partText = details ? part.text.trimStart() : lastThinkingLines(part.text.trimStart());
|
|
52
|
-
output +=
|
|
46
|
+
output += `${ansi.BOLD_BRIGHT_BLACK}~${ansi.RESET} ${ansi.BRIGHT_BLACK}${partText}${ansi.RESET}\n\n`;
|
|
53
47
|
} else if (part.title === "response") {
|
|
48
|
+
// Show all response lines
|
|
54
49
|
const doc = parse(part.text.trimStart(), gfm);
|
|
55
50
|
const partText = renderToConsole(doc);
|
|
56
|
-
output +=
|
|
57
|
-
} else if (part.title === "tool") {
|
|
58
|
-
|
|
59
|
-
} else if (part.title === "files") {
|
|
51
|
+
output += `${ansi.WHITE_BACKGROUND}${ansi.BOLD_BLACK}*${ansi.RESET} ${partText}\n\n`;
|
|
52
|
+
} else if (part.title === "tool" || part.title === "files") {
|
|
53
|
+
// TODO: Show max 10 tool/file lines?
|
|
60
54
|
output += part.text + "\n\n";
|
|
61
55
|
} else if (part.title === "todo") {
|
|
56
|
+
// Show the whole todo list
|
|
62
57
|
output += part.text + "\n\n";
|
|
63
58
|
}
|
|
64
59
|
}
|
|
@@ -84,6 +79,9 @@ export function render(state: State, details = false): void {
|
|
|
84
79
|
}
|
|
85
80
|
|
|
86
81
|
state.renderedLines = lines;
|
|
82
|
+
} else if (state.renderedLines.length > 0) {
|
|
83
|
+
clearRenderedLines(state, state.renderedLines.length);
|
|
84
|
+
state.renderedLines = [];
|
|
87
85
|
}
|
|
88
86
|
}
|
|
89
87
|
|
|
@@ -130,15 +128,17 @@ function clearRenderedLines(state: State, linesToClear: number): void {
|
|
|
130
128
|
}
|
|
131
129
|
|
|
132
130
|
export function wrapText(text: string, width: number): string[] {
|
|
131
|
+
const INDENT = " ";
|
|
132
|
+
const indentLength = INDENT.length;
|
|
133
133
|
const lines: string[] = [];
|
|
134
|
-
let currentLine =
|
|
135
|
-
let visibleLength =
|
|
134
|
+
let currentLine = INDENT;
|
|
135
|
+
let visibleLength = indentLength;
|
|
136
136
|
let i = 0;
|
|
137
137
|
|
|
138
138
|
const pushLine = () => {
|
|
139
139
|
lines.push(currentLine);
|
|
140
|
-
currentLine =
|
|
141
|
-
visibleLength =
|
|
140
|
+
currentLine = INDENT;
|
|
141
|
+
visibleLength = indentLength;
|
|
142
142
|
};
|
|
143
143
|
|
|
144
144
|
const addWord = (word: string, wordVisibleLength: number) => {
|
|
@@ -150,21 +150,21 @@ export function wrapText(text: string, width: number): string[] {
|
|
|
150
150
|
: visibleLength + 1 + wordVisibleLength <= width;
|
|
151
151
|
|
|
152
152
|
if (wouldFit) {
|
|
153
|
-
if (visibleLength >
|
|
153
|
+
if (visibleLength > indentLength) {
|
|
154
154
|
currentLine += " ";
|
|
155
155
|
visibleLength++;
|
|
156
156
|
}
|
|
157
157
|
currentLine += word;
|
|
158
158
|
visibleLength += wordVisibleLength;
|
|
159
|
-
} else if (visibleLength >
|
|
159
|
+
} else if (visibleLength > indentLength) {
|
|
160
160
|
pushLine();
|
|
161
|
-
currentLine = word;
|
|
162
|
-
visibleLength = wordVisibleLength;
|
|
161
|
+
currentLine = INDENT + word;
|
|
162
|
+
visibleLength = indentLength + wordVisibleLength;
|
|
163
163
|
} else if (wordVisibleLength <= width) {
|
|
164
|
-
currentLine = word;
|
|
165
|
-
visibleLength = wordVisibleLength;
|
|
164
|
+
currentLine = INDENT + word;
|
|
165
|
+
visibleLength = indentLength + wordVisibleLength;
|
|
166
166
|
} else {
|
|
167
|
-
const wordWidth = width;
|
|
167
|
+
const wordWidth = width - indentLength;
|
|
168
168
|
for (let w = 0; w < word.length; ) {
|
|
169
169
|
let segment = "";
|
|
170
170
|
let segmentVisible = 0;
|
|
@@ -192,8 +192,8 @@ export function wrapText(text: string, width: number): string[] {
|
|
|
192
192
|
if (currentLine) {
|
|
193
193
|
pushLine();
|
|
194
194
|
}
|
|
195
|
-
currentLine = segment;
|
|
196
|
-
visibleLength = segmentVisible;
|
|
195
|
+
currentLine = INDENT + segment;
|
|
196
|
+
visibleLength = indentLength + segmentVisible;
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
}
|
|
@@ -237,7 +237,7 @@ export function wrapText(text: string, width: number): string[] {
|
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
if (currentLine || lines.length === 0) {
|
|
240
|
+
if (currentLine.trim() || lines.length === 0) {
|
|
241
241
|
pushLine();
|
|
242
242
|
}
|
|
243
243
|
|
|
@@ -252,18 +252,40 @@ export function writePrompt(): void {
|
|
|
252
252
|
|
|
253
253
|
const ANIMATION_CHARS = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇"];
|
|
254
254
|
let animationInterval: ReturnType<typeof setInterval> | null = null;
|
|
255
|
+
let requestStartTime: number | null = null;
|
|
255
256
|
|
|
256
|
-
export function startAnimation(): void {
|
|
257
|
+
export function startAnimation(startTime?: number): void {
|
|
257
258
|
if (animationInterval) return;
|
|
258
259
|
|
|
260
|
+
requestStartTime = startTime || Date.now();
|
|
261
|
+
|
|
259
262
|
let index = 0;
|
|
260
263
|
animationInterval = setInterval(() => {
|
|
261
|
-
|
|
262
|
-
|
|
264
|
+
const elapsed = Date.now() - requestStartTime!;
|
|
265
|
+
const elapsedText = formatDuration(elapsed);
|
|
266
|
+
|
|
267
|
+
process.stdout.write(
|
|
268
|
+
`\r${ansi.BOLD_MAGENTA}${ANIMATION_CHARS[index]} ${ansi.RESET}${ansi.BRIGHT_BLACK}Running for ${elapsedText}${ansi.RESET}`,
|
|
269
|
+
);
|
|
263
270
|
index = (index + 1) % ANIMATION_CHARS.length;
|
|
264
271
|
}, 100);
|
|
265
272
|
}
|
|
266
273
|
|
|
274
|
+
function formatDuration(ms: number): string {
|
|
275
|
+
const seconds = ms / 1000;
|
|
276
|
+
if (seconds < 60) {
|
|
277
|
+
return `${Math.round(seconds)}s`;
|
|
278
|
+
}
|
|
279
|
+
const minutes = Math.floor(seconds / 60);
|
|
280
|
+
const remainingSeconds = Math.round(seconds % 60);
|
|
281
|
+
if (minutes < 60) {
|
|
282
|
+
return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
|
|
283
|
+
}
|
|
284
|
+
const hours = Math.floor(minutes / 60);
|
|
285
|
+
const remainingMinutes = minutes % 60;
|
|
286
|
+
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
|
|
287
|
+
}
|
|
288
|
+
|
|
267
289
|
export function stopAnimation(): void {
|
|
268
290
|
if (animationInterval) {
|
|
269
291
|
clearInterval(animationInterval);
|