@phren/agent 0.1.2 → 0.1.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/dist/agent-loop.js +9 -2
- package/dist/commands.js +351 -4
- package/dist/config.js +6 -2
- package/dist/index.js +1 -0
- package/dist/multi/spawner.js +3 -2
- package/dist/permissions/shell-safety.js +8 -0
- package/dist/providers/anthropic.js +68 -31
- package/dist/providers/codex.js +112 -56
- package/dist/repl.js +2 -2
- package/dist/system-prompt.js +24 -26
- package/dist/tools/shell.js +5 -2
- package/dist/tui.js +288 -31
- package/package.json +2 -2
package/dist/tui.js
CHANGED
|
@@ -9,6 +9,8 @@ import { handleCommand } from "./commands.js";
|
|
|
9
9
|
import { renderMarkdown } from "./multi/markdown.js";
|
|
10
10
|
import { decodeDiffPayload, renderInlineDiff, DIFF_MARKER } from "./multi/diff-renderer.js";
|
|
11
11
|
import * as os from "os";
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
12
14
|
import { execSync } from "node:child_process";
|
|
13
15
|
import { loadInputMode, saveInputMode, savePermissionMode } from "./settings.js";
|
|
14
16
|
import { createRequire } from "node:module";
|
|
@@ -144,10 +146,12 @@ export async function startTui(config, spawner) {
|
|
|
144
146
|
const session = createSession(contextLimit);
|
|
145
147
|
const w = process.stdout;
|
|
146
148
|
const isTTY = process.stdout.isTTY;
|
|
149
|
+
const startTime = Date.now();
|
|
147
150
|
let inputMode = loadInputMode();
|
|
148
151
|
let pendingInput = null;
|
|
149
152
|
let running = false;
|
|
150
153
|
let inputLine = "";
|
|
154
|
+
let cursorPos = 0;
|
|
151
155
|
let costStr = "";
|
|
152
156
|
// ── Dual-mode state ─────────────────────────────────────────────────────
|
|
153
157
|
let tuiMode = "chat";
|
|
@@ -190,6 +194,7 @@ export async function startTui(config, spawner) {
|
|
|
190
194
|
menuFilterActive = false;
|
|
191
195
|
menuFilterBuf = "";
|
|
192
196
|
w.write("\x1b[?1049l"); // leave alternate screen (restores chat)
|
|
197
|
+
setScrollRegion(); // re-establish scroll region after alt screen
|
|
193
198
|
statusBar();
|
|
194
199
|
prompt(true); // skip newline — alt screen restore already positioned cursor
|
|
195
200
|
}
|
|
@@ -210,16 +215,65 @@ export async function startTui(config, spawner) {
|
|
|
210
215
|
const icon = PERMISSION_ICONS[mode];
|
|
211
216
|
const rows = process.stdout.rows || 24;
|
|
212
217
|
const c = cols();
|
|
213
|
-
if (!skipNewline)
|
|
218
|
+
if (!skipNewline) {
|
|
219
|
+
// Newline within the scroll region so content scrolls up naturally
|
|
220
|
+
cursorToScrollEnd();
|
|
214
221
|
w.write("\n");
|
|
215
|
-
|
|
222
|
+
}
|
|
223
|
+
// Draw the fixed bottom bar outside the scroll region.
|
|
224
|
+
// Temporarily reset DECSTBM so writes to rows (rows-4)..(rows) work.
|
|
225
|
+
w.write(`${ESC}r`); // reset scroll region temporarily
|
|
226
|
+
// Layout (bottom up): blank, permissions, separator, input, separator
|
|
227
|
+
const sep = s.dim("─".repeat(c));
|
|
216
228
|
const permLine = ` ${color(`${icon} ${PERMISSION_LABELS[mode]} permissions`)} ${s.dim("(shift+tab to cycle)")}`;
|
|
217
|
-
w.write(`${ESC}${rows -
|
|
229
|
+
w.write(`${ESC}${rows - 4};1H${ESC}2K${sep}`);
|
|
230
|
+
w.write(`${ESC}${rows - 3};1H${ESC}2K${bashMode ? `${s.yellow("!")} ` : `${s.dim("▸")} `}`);
|
|
231
|
+
w.write(`${ESC}${rows - 2};1H${ESC}2K${sep}`);
|
|
218
232
|
w.write(`${ESC}${rows - 1};1H${ESC}2K${permLine}`);
|
|
219
|
-
w.write(`${ESC}${rows};1H${ESC}2K
|
|
233
|
+
w.write(`${ESC}${rows};1H${ESC}2K`); // blank bottom row
|
|
234
|
+
// Re-establish scroll region and position cursor at the input line
|
|
235
|
+
setScrollRegion();
|
|
236
|
+
w.write(`${ESC}${rows - 3};${bashMode ? 3 : 4}H`);
|
|
237
|
+
}
|
|
238
|
+
// Redraw the input line and position the terminal cursor at cursorPos
|
|
239
|
+
function redrawInput() {
|
|
240
|
+
w.write(`${ESC}2K\r`);
|
|
241
|
+
prompt(true);
|
|
242
|
+
w.write(inputLine);
|
|
243
|
+
// Move terminal cursor back from end to cursorPos
|
|
244
|
+
const back = inputLine.length - cursorPos;
|
|
245
|
+
if (back > 0)
|
|
246
|
+
w.write(`${ESC}${back}D`);
|
|
247
|
+
}
|
|
248
|
+
// ── Scroll region management ─────────────────────────────────────────
|
|
249
|
+
// DECSTBM: rows 1..(rows-5) scroll; bottom 5 rows are fixed for the input bar.
|
|
250
|
+
function setScrollRegion() {
|
|
251
|
+
if (!isTTY)
|
|
252
|
+
return;
|
|
253
|
+
const rows = process.stdout.rows || 24;
|
|
254
|
+
const scrollBottom = Math.max(1, rows - 5);
|
|
255
|
+
w.write(`${ESC}1;${scrollBottom}r`);
|
|
256
|
+
}
|
|
257
|
+
// Move cursor to the bottom of the scroll region so new output scrolls naturally.
|
|
258
|
+
function cursorToScrollEnd() {
|
|
259
|
+
if (!isTTY)
|
|
260
|
+
return;
|
|
261
|
+
const rows = process.stdout.rows || 24;
|
|
262
|
+
const scrollBottom = Math.max(1, rows - 5);
|
|
263
|
+
w.write(`${ESC}${scrollBottom};1H`);
|
|
220
264
|
}
|
|
265
|
+
// Periodic status bar refresh (every 30s) — keeps cost/turns current during long tool runs
|
|
266
|
+
const statusRefreshTimer = isTTY
|
|
267
|
+
? setInterval(() => { if (tuiMode === "chat")
|
|
268
|
+
statusBar(); }, 30_000)
|
|
269
|
+
: null;
|
|
270
|
+
if (statusRefreshTimer)
|
|
271
|
+
statusRefreshTimer.unref(); // don't keep process alive
|
|
221
272
|
// Terminal cleanup: restore state on exit
|
|
222
273
|
function cleanupTerminal() {
|
|
274
|
+
if (statusRefreshTimer)
|
|
275
|
+
clearInterval(statusRefreshTimer);
|
|
276
|
+
w.write(`${ESC}r`); // reset scroll region
|
|
223
277
|
w.write("\x1b[?1049l"); // leave alt screen if active
|
|
224
278
|
if (process.stdin.isTTY) {
|
|
225
279
|
try {
|
|
@@ -229,6 +283,15 @@ export async function startTui(config, spawner) {
|
|
|
229
283
|
}
|
|
230
284
|
}
|
|
231
285
|
process.on("exit", cleanupTerminal);
|
|
286
|
+
// Re-establish scroll region on terminal resize.
|
|
287
|
+
// Node's "resize" event already fires on SIGWINCH — no separate signal handler needed.
|
|
288
|
+
process.stdout.on("resize", () => {
|
|
289
|
+
if (tuiMode === "chat") {
|
|
290
|
+
setScrollRegion();
|
|
291
|
+
statusBar();
|
|
292
|
+
prompt(true);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
232
295
|
// Setup: clear screen, status bar at top, content area clean
|
|
233
296
|
if (isTTY) {
|
|
234
297
|
w.write(`${ESC}2J${ESC}H`); // clear entire screen + home
|
|
@@ -266,6 +329,7 @@ export async function startTui(config, spawner) {
|
|
|
266
329
|
w.write(`\n ${info[0]}\n ${info[1]} ${info[2]}\n ${info[4]}\n\n ${info[6]}\n\n`);
|
|
267
330
|
}
|
|
268
331
|
w.write("\n");
|
|
332
|
+
setScrollRegion(); // establish scroll region after banner
|
|
269
333
|
}
|
|
270
334
|
// Raw stdin for steering
|
|
271
335
|
if (process.stdin.isTTY) {
|
|
@@ -347,8 +411,83 @@ export async function startTui(config, spawner) {
|
|
|
347
411
|
prompt(true);
|
|
348
412
|
return;
|
|
349
413
|
}
|
|
350
|
-
// Tab — toggle mode
|
|
351
|
-
if (key.name === "tab" && !menuFilterActive) {
|
|
414
|
+
// Tab — completion or toggle mode
|
|
415
|
+
if (key.name === "tab" && !key.shift && !menuFilterActive) {
|
|
416
|
+
// Slash command completion in chat mode
|
|
417
|
+
if (tuiMode === "chat" && inputLine.startsWith("/")) {
|
|
418
|
+
const SLASH_COMMANDS = [
|
|
419
|
+
"/help", "/model", "/provider", "/turns", "/clear", "/cost",
|
|
420
|
+
"/plan", "/undo", "/history", "/compact", "/context", "/mode",
|
|
421
|
+
"/spawn", "/agents", "/diff", "/git", "/files", "/cwd",
|
|
422
|
+
"/preset", "/exit",
|
|
423
|
+
];
|
|
424
|
+
const matches = SLASH_COMMANDS.filter((c) => c.startsWith(inputLine));
|
|
425
|
+
if (matches.length === 1) {
|
|
426
|
+
inputLine = matches[0];
|
|
427
|
+
cursorPos = inputLine.length;
|
|
428
|
+
redrawInput();
|
|
429
|
+
}
|
|
430
|
+
else if (matches.length > 1) {
|
|
431
|
+
// Show matches in scroll area, then redraw prompt
|
|
432
|
+
cursorToScrollEnd();
|
|
433
|
+
w.write(`\n${s.dim(" " + matches.join(" "))}\n`);
|
|
434
|
+
prompt(true);
|
|
435
|
+
w.write(inputLine);
|
|
436
|
+
const back = inputLine.length - cursorPos;
|
|
437
|
+
if (back > 0)
|
|
438
|
+
w.write(`${ESC}${back}D`);
|
|
439
|
+
}
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
// File path completion in bash mode
|
|
443
|
+
if (tuiMode === "chat" && bashMode && inputLine.length > 0) {
|
|
444
|
+
// Complete the last whitespace-delimited token as a path
|
|
445
|
+
const lastSpace = inputLine.lastIndexOf(" ");
|
|
446
|
+
const prefix = lastSpace === -1 ? "" : inputLine.slice(0, lastSpace + 1);
|
|
447
|
+
const partial = lastSpace === -1 ? inputLine : inputLine.slice(lastSpace + 1);
|
|
448
|
+
const expandedPartial = partial.replace(/^~/, os.homedir());
|
|
449
|
+
const dir = path.dirname(expandedPartial);
|
|
450
|
+
const base = path.basename(expandedPartial);
|
|
451
|
+
try {
|
|
452
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
453
|
+
const matches = entries.filter((e) => e.name.startsWith(base));
|
|
454
|
+
if (matches.length === 1) {
|
|
455
|
+
const completed = matches[0];
|
|
456
|
+
const fullPath = partial.startsWith("~")
|
|
457
|
+
? "~/" + path.relative(os.homedir(), path.join(dir, completed.name))
|
|
458
|
+
: path.join(dir, completed.name);
|
|
459
|
+
inputLine = prefix + fullPath + (completed.isDirectory() ? "/" : "");
|
|
460
|
+
cursorPos = inputLine.length;
|
|
461
|
+
redrawInput();
|
|
462
|
+
}
|
|
463
|
+
else if (matches.length > 1) {
|
|
464
|
+
const names = matches.map((e) => e.name + (e.isDirectory() ? "/" : ""));
|
|
465
|
+
cursorToScrollEnd();
|
|
466
|
+
w.write(`\n${s.dim(" " + names.join(" "))}\n`);
|
|
467
|
+
// Find longest common prefix for partial completion
|
|
468
|
+
let common = matches[0].name;
|
|
469
|
+
for (const m of matches) {
|
|
470
|
+
while (!m.name.startsWith(common))
|
|
471
|
+
common = common.slice(0, -1);
|
|
472
|
+
}
|
|
473
|
+
if (common.length > base.length) {
|
|
474
|
+
const fullPath = partial.startsWith("~")
|
|
475
|
+
? "~/" + path.relative(os.homedir(), path.join(dir, common))
|
|
476
|
+
: path.join(dir, common);
|
|
477
|
+
inputLine = prefix + fullPath;
|
|
478
|
+
cursorPos = inputLine.length;
|
|
479
|
+
}
|
|
480
|
+
prompt(true);
|
|
481
|
+
w.write(inputLine);
|
|
482
|
+
const back = inputLine.length - cursorPos;
|
|
483
|
+
if (back > 0)
|
|
484
|
+
w.write(`${ESC}${back}D`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
catch { /* dir doesn't exist or unreadable */ }
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
// Default: toggle menu mode
|
|
352
491
|
if (tuiMode === "chat" && !running) {
|
|
353
492
|
enterMenuMode();
|
|
354
493
|
}
|
|
@@ -368,11 +507,13 @@ export async function startTui(config, spawner) {
|
|
|
368
507
|
if (bashMode) {
|
|
369
508
|
bashMode = false;
|
|
370
509
|
inputLine = "";
|
|
510
|
+
cursorPos = 0;
|
|
371
511
|
prompt(true);
|
|
372
512
|
return;
|
|
373
513
|
}
|
|
374
514
|
if (inputLine) {
|
|
375
515
|
inputLine = "";
|
|
516
|
+
cursorPos = 0;
|
|
376
517
|
prompt(true);
|
|
377
518
|
return;
|
|
378
519
|
}
|
|
@@ -389,6 +530,7 @@ export async function startTui(config, spawner) {
|
|
|
389
530
|
if (bashMode) {
|
|
390
531
|
bashMode = false;
|
|
391
532
|
inputLine = "";
|
|
533
|
+
cursorPos = 0;
|
|
392
534
|
prompt(true);
|
|
393
535
|
ctrlCCount = 0;
|
|
394
536
|
return;
|
|
@@ -396,6 +538,7 @@ export async function startTui(config, spawner) {
|
|
|
396
538
|
if (inputLine) {
|
|
397
539
|
// Clear input
|
|
398
540
|
inputLine = "";
|
|
541
|
+
cursorPos = 0;
|
|
399
542
|
prompt(true);
|
|
400
543
|
ctrlCCount = 0;
|
|
401
544
|
return;
|
|
@@ -410,8 +553,7 @@ export async function startTui(config, spawner) {
|
|
|
410
553
|
}
|
|
411
554
|
else {
|
|
412
555
|
// Actually quit
|
|
413
|
-
|
|
414
|
-
process.stdin.setRawMode(false);
|
|
556
|
+
cleanupTerminal();
|
|
415
557
|
w.write(s.dim("\nSession ended.\n"));
|
|
416
558
|
resolve(session);
|
|
417
559
|
}
|
|
@@ -420,7 +562,10 @@ export async function startTui(config, spawner) {
|
|
|
420
562
|
// Enter — submit
|
|
421
563
|
if (key.name === "return") {
|
|
422
564
|
const line = inputLine.trim();
|
|
565
|
+
cursorPos = 0;
|
|
423
566
|
inputLine = "";
|
|
567
|
+
// Move to the bottom of the scroll region so new output scrolls naturally
|
|
568
|
+
cursorToScrollEnd();
|
|
424
569
|
w.write("\n");
|
|
425
570
|
if (!line) {
|
|
426
571
|
prompt();
|
|
@@ -473,13 +618,19 @@ export async function startTui(config, spawner) {
|
|
|
473
618
|
prompt();
|
|
474
619
|
return;
|
|
475
620
|
}
|
|
476
|
-
|
|
621
|
+
const cmdResult = handleCommand(line, {
|
|
477
622
|
session,
|
|
478
623
|
contextLimit,
|
|
479
624
|
undoStack: [],
|
|
480
625
|
providerName: config.provider.name,
|
|
481
626
|
currentModel: config.provider.model,
|
|
627
|
+
provider: config.provider,
|
|
628
|
+
systemPrompt: config.systemPrompt,
|
|
482
629
|
spawner,
|
|
630
|
+
sessionId: config.sessionId,
|
|
631
|
+
startTime,
|
|
632
|
+
phrenPath: config.phrenCtx?.phrenPath,
|
|
633
|
+
phrenCtx: config.phrenCtx,
|
|
483
634
|
onModelChange: (result) => {
|
|
484
635
|
// Live model switch — re-resolve provider with new model
|
|
485
636
|
try {
|
|
@@ -494,10 +645,15 @@ export async function startTui(config, spawner) {
|
|
|
494
645
|
}
|
|
495
646
|
catch { /* keep current provider on error */ }
|
|
496
647
|
},
|
|
497
|
-
})
|
|
648
|
+
});
|
|
649
|
+
if (cmdResult === true) {
|
|
498
650
|
prompt();
|
|
499
651
|
return;
|
|
500
652
|
}
|
|
653
|
+
if (typeof cmdResult === "object" && cmdResult instanceof Promise) {
|
|
654
|
+
cmdResult.then(() => { prompt(); });
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
501
657
|
// If agent is running, buffer input
|
|
502
658
|
if (running) {
|
|
503
659
|
pendingInput = line;
|
|
@@ -521,9 +677,8 @@ export async function startTui(config, spawner) {
|
|
|
521
677
|
historyIndex--;
|
|
522
678
|
}
|
|
523
679
|
inputLine = inputHistory[historyIndex];
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
w.write(inputLine);
|
|
680
|
+
cursorPos = inputLine.length;
|
|
681
|
+
redrawInput();
|
|
527
682
|
return;
|
|
528
683
|
}
|
|
529
684
|
// Down arrow — next history or restore saved
|
|
@@ -538,20 +693,112 @@ export async function startTui(config, spawner) {
|
|
|
538
693
|
historyIndex = -1;
|
|
539
694
|
inputLine = savedInput;
|
|
540
695
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
w.write(inputLine);
|
|
696
|
+
cursorPos = inputLine.length;
|
|
697
|
+
redrawInput();
|
|
544
698
|
return;
|
|
545
699
|
}
|
|
546
|
-
//
|
|
700
|
+
// Ctrl+A — move cursor to start of line
|
|
701
|
+
if (key.ctrl && key.name === "a") {
|
|
702
|
+
cursorPos = 0;
|
|
703
|
+
redrawInput();
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
// Ctrl+E — move cursor to end of line
|
|
707
|
+
if (key.ctrl && key.name === "e") {
|
|
708
|
+
cursorPos = inputLine.length;
|
|
709
|
+
redrawInput();
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
// Ctrl+U — kill entire line
|
|
713
|
+
if (key.ctrl && key.name === "u") {
|
|
714
|
+
inputLine = "";
|
|
715
|
+
cursorPos = 0;
|
|
716
|
+
redrawInput();
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
// Ctrl+K — kill from cursor to end of line
|
|
720
|
+
if (key.ctrl && key.name === "k") {
|
|
721
|
+
inputLine = inputLine.slice(0, cursorPos);
|
|
722
|
+
redrawInput();
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
// Left arrow — move cursor left one character
|
|
726
|
+
if (key.name === "left" && !key.meta && !key.ctrl) {
|
|
727
|
+
if (cursorPos > 0) {
|
|
728
|
+
cursorPos--;
|
|
729
|
+
w.write(`${ESC}D`);
|
|
730
|
+
}
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
// Right arrow — move cursor right one character
|
|
734
|
+
if (key.name === "right" && !key.meta && !key.ctrl) {
|
|
735
|
+
if (cursorPos < inputLine.length) {
|
|
736
|
+
cursorPos++;
|
|
737
|
+
w.write(`${ESC}C`);
|
|
738
|
+
}
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
// Alt+Left — move cursor left by one word
|
|
742
|
+
if (key.name === "left" && (key.meta || key.ctrl)) {
|
|
743
|
+
if (cursorPos > 0) {
|
|
744
|
+
// Skip spaces, then skip non-spaces
|
|
745
|
+
let p = cursorPos;
|
|
746
|
+
while (p > 0 && inputLine[p - 1] === " ")
|
|
747
|
+
p--;
|
|
748
|
+
while (p > 0 && inputLine[p - 1] !== " ")
|
|
749
|
+
p--;
|
|
750
|
+
cursorPos = p;
|
|
751
|
+
redrawInput();
|
|
752
|
+
}
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
// Alt+Right — move cursor right by one word
|
|
756
|
+
if (key.name === "right" && (key.meta || key.ctrl)) {
|
|
757
|
+
if (cursorPos < inputLine.length) {
|
|
758
|
+
let p = cursorPos;
|
|
759
|
+
while (p < inputLine.length && inputLine[p] !== " ")
|
|
760
|
+
p++;
|
|
761
|
+
while (p < inputLine.length && inputLine[p] === " ")
|
|
762
|
+
p++;
|
|
763
|
+
cursorPos = p;
|
|
764
|
+
redrawInput();
|
|
765
|
+
}
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
// Word-delete: Alt+Backspace, Ctrl+Backspace, Ctrl+W
|
|
769
|
+
if (((key.meta || key.ctrl) && key.name === "backspace") ||
|
|
770
|
+
(key.ctrl && key.name === "w")) {
|
|
771
|
+
if (cursorPos > 0) {
|
|
772
|
+
// Find word boundary before cursor
|
|
773
|
+
let p = cursorPos;
|
|
774
|
+
while (p > 0 && inputLine[p - 1] === " ")
|
|
775
|
+
p--;
|
|
776
|
+
while (p > 0 && inputLine[p - 1] !== " ")
|
|
777
|
+
p--;
|
|
778
|
+
inputLine = inputLine.slice(0, p) + inputLine.slice(cursorPos);
|
|
779
|
+
cursorPos = p;
|
|
780
|
+
redrawInput();
|
|
781
|
+
}
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
// Backspace — delete character before cursor
|
|
547
785
|
if (key.name === "backspace") {
|
|
548
|
-
if (
|
|
549
|
-
inputLine = inputLine.slice(0, -1);
|
|
550
|
-
|
|
786
|
+
if (cursorPos > 0) {
|
|
787
|
+
inputLine = inputLine.slice(0, cursorPos - 1) + inputLine.slice(cursorPos);
|
|
788
|
+
cursorPos--;
|
|
789
|
+
redrawInput();
|
|
790
|
+
}
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
// Delete — delete character at cursor
|
|
794
|
+
if (key.name === "delete") {
|
|
795
|
+
if (cursorPos < inputLine.length) {
|
|
796
|
+
inputLine = inputLine.slice(0, cursorPos) + inputLine.slice(cursorPos + 1);
|
|
797
|
+
redrawInput();
|
|
551
798
|
}
|
|
552
799
|
return;
|
|
553
800
|
}
|
|
554
|
-
// Regular character
|
|
801
|
+
// Regular character — insert at cursor position
|
|
555
802
|
if (key.sequence && !key.ctrl && !key.meta) {
|
|
556
803
|
// ! at start of empty input toggles bash mode
|
|
557
804
|
if (key.sequence === "!" && inputLine === "" && !bashMode) {
|
|
@@ -559,8 +806,9 @@ export async function startTui(config, spawner) {
|
|
|
559
806
|
prompt(true);
|
|
560
807
|
return;
|
|
561
808
|
}
|
|
562
|
-
inputLine
|
|
563
|
-
|
|
809
|
+
inputLine = inputLine.slice(0, cursorPos) + key.sequence + inputLine.slice(cursorPos);
|
|
810
|
+
cursorPos += key.sequence.length;
|
|
811
|
+
redrawInput();
|
|
564
812
|
}
|
|
565
813
|
});
|
|
566
814
|
// TUI hooks — render streaming text with markdown, compact tool output
|
|
@@ -578,11 +826,8 @@ export async function startTui(config, spawner) {
|
|
|
578
826
|
w.write(`${ESC}2K\r`); // clear thinking timer line
|
|
579
827
|
firstDelta = false;
|
|
580
828
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
if (textBuffer.includes("\n\n") || textBuffer.endsWith("\n")) {
|
|
584
|
-
flushTextBuffer();
|
|
585
|
-
}
|
|
829
|
+
// Stream directly for real-time feel — write each delta immediately
|
|
830
|
+
w.write(text);
|
|
586
831
|
},
|
|
587
832
|
onTextDone: () => {
|
|
588
833
|
flushTextBuffer();
|
|
@@ -621,14 +866,26 @@ export async function startTui(config, spawner) {
|
|
|
621
866
|
async function runAgentTurn(userInput) {
|
|
622
867
|
running = true;
|
|
623
868
|
firstDelta = true;
|
|
869
|
+
cursorToScrollEnd(); // ensure all turn output stays within scroll region
|
|
624
870
|
const thinkStart = Date.now();
|
|
871
|
+
// Phren thinking — subtle purple/cyan breath, no spinner gimmicks
|
|
872
|
+
let thinkFrame = 0;
|
|
625
873
|
const thinkTimer = setInterval(() => {
|
|
626
|
-
const elapsed = (
|
|
627
|
-
|
|
628
|
-
|
|
874
|
+
const elapsed = (Date.now() - thinkStart) / 1000;
|
|
875
|
+
// Gentle sine-wave interpolation between phren purple and cyan
|
|
876
|
+
const t = (Math.sin(thinkFrame * 0.08) + 1) / 2; // 0..1, slow oscillation
|
|
877
|
+
const r = Math.round(155 * (1 - t) + 40 * t);
|
|
878
|
+
const g = Math.round(140 * (1 - t) + 211 * t);
|
|
879
|
+
const b = Math.round(250 * (1 - t) + 242 * t);
|
|
880
|
+
const color = `${ESC}38;2;${r};${g};${b}m`;
|
|
881
|
+
w.write(`${ESC}2K ${color}◆ thinking${ESC}0m ${s.dim(`${elapsed.toFixed(1)}s`)}\r`);
|
|
882
|
+
thinkFrame++;
|
|
883
|
+
}, 50);
|
|
629
884
|
try {
|
|
630
885
|
await runTurn(userInput, session, config, tuiHooks);
|
|
631
886
|
clearInterval(thinkTimer);
|
|
887
|
+
const elapsed = ((Date.now() - thinkStart) / 1000).toFixed(1);
|
|
888
|
+
w.write(`${ESC}2K ${s.dim(`◆ thought for ${elapsed}s`)}\n`);
|
|
632
889
|
statusBar();
|
|
633
890
|
}
|
|
634
891
|
catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phren/agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Coding agent with persistent memory — powered by phren",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"dist"
|
|
14
14
|
],
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@phren/cli": "0.1.
|
|
16
|
+
"@phren/cli": "0.1.3"
|
|
17
17
|
},
|
|
18
18
|
"engines": {
|
|
19
19
|
"node": ">=20.0.0"
|