premanmcp 1.1.7 → 1.1.8
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/bin/agent.js +295 -22
- package/package.json +1 -1
package/bin/agent.js
CHANGED
|
@@ -84,12 +84,13 @@ const THINK_MS = 220;
|
|
|
84
84
|
const CLEAR_LINE = "\r\u001b[2K";
|
|
85
85
|
const CLEAR_SCREEN = "\u001b[2J\u001b[H";
|
|
86
86
|
|
|
87
|
-
// The alternate screen buffer
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
87
|
+
// The alternate screen buffer is deliberately not used, and the sequences for
|
|
88
|
+
// it are deliberately not defined, so that reaching for one is a decision
|
|
89
|
+
// rather than an autocomplete. It gave a session a screen of its own -- a
|
|
90
|
+
// second `preman` could not be scrolled back into the first -- by holding
|
|
91
|
+
// exactly one screen and keeping no history, which meant a reply that ran past
|
|
92
|
+
// the top of the window could not be scrolled back to either. A transcript is
|
|
93
|
+
// the wrong thing to put in a buffer that forgets.
|
|
93
94
|
const SHOW_CURSOR = "\u001b[?25h";
|
|
94
95
|
const RELEASE_REGION = "\u001b[r";
|
|
95
96
|
|
|
@@ -217,6 +218,188 @@ export function artifactLine(artifact) {
|
|
|
217
218
|
return url ? `${truncate(label, 72)} — ${url}` : truncate(label, 100);
|
|
218
219
|
}
|
|
219
220
|
|
|
221
|
+
/**
|
|
222
|
+
* The route an endpoint sits on, normalised so two saved requests that hit the
|
|
223
|
+
* same place collapse onto one line.
|
|
224
|
+
*
|
|
225
|
+
* The same rule the app groups by, deliberately: a workspace that reads as 84
|
|
226
|
+
* routes in the browser and 284 rows here is two different answers to one
|
|
227
|
+
* question. A saved URL is often a template (`{{baseUrl}}/v1/users`) rather
|
|
228
|
+
* than something `URL` will parse, which is why the fallback strips the
|
|
229
|
+
* template head by hand instead of giving up.
|
|
230
|
+
*/
|
|
231
|
+
export function endpointRoutePath(endpoint) {
|
|
232
|
+
const raw = String(endpoint?.url || endpoint?.name || "/").trim();
|
|
233
|
+
let path = raw;
|
|
234
|
+
try {
|
|
235
|
+
path = new URL(raw).pathname || "/";
|
|
236
|
+
} catch {
|
|
237
|
+
path = raw.replace(/^\{\{[^}]+\}\}/, "").split(/[?#]/, 1)[0] || "/";
|
|
238
|
+
}
|
|
239
|
+
if (!path.startsWith("/")) path = `/${path}`;
|
|
240
|
+
return path.length > 1 ? path.replace(/\/+$/, "") : path;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// The three buckets the server counts in and the model writes its reply from.
|
|
244
|
+
// Matching them exactly is what keeps "87 failing" in the prose and the heading
|
|
245
|
+
// above the rows from being two different numbers.
|
|
246
|
+
const FAILING_RUNS = new Set(["failed", "error", "fail"]);
|
|
247
|
+
const PASSING_RUNS = new Set(["passed", "pass", "ok", "success"]);
|
|
248
|
+
|
|
249
|
+
/** Which of the three buckets a last-run status falls in. */
|
|
250
|
+
export function endpointBucket(lastRun) {
|
|
251
|
+
const status = String(lastRun || "").trim().toLowerCase();
|
|
252
|
+
if (!status) return "untested";
|
|
253
|
+
if (FAILING_RUNS.has(status)) return "failing";
|
|
254
|
+
if (PASSING_RUNS.has(status)) return "passing";
|
|
255
|
+
return "untested";
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Failing first because it is the only bucket anybody acts on, then untested,
|
|
259
|
+
// then passing -- the order the model already writes its own summary in.
|
|
260
|
+
const BUCKET_ORDER = ["failing", "untested", "passing"];
|
|
261
|
+
const BUCKET_LABELS = { failing: "Failing", untested: "Untested", passing: "Passing" };
|
|
262
|
+
const METHOD_ORDER = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
263
|
+
|
|
264
|
+
const methodRank = (method) => {
|
|
265
|
+
const at = METHOD_ORDER.indexOf(String(method || "GET").trim().toUpperCase());
|
|
266
|
+
return at === -1 ? METHOD_ORDER.length : at;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Every endpoint the turn returned, once, as routes.
|
|
271
|
+
*
|
|
272
|
+
* A list of 284 arrives as thirteen separate tool calls, because the tool caps
|
|
273
|
+
* a page at 25 rows and the model pages through it. Ids are deduplicated
|
|
274
|
+
* because overlapping pages are a model's decision and not a thing to render
|
|
275
|
+
* twice; routes are collapsed because that is what turns request history back
|
|
276
|
+
* into an API. A route whose saved requests disagree about their last run
|
|
277
|
+
* takes the worst of them, since "one of these is failing" is the fact worth
|
|
278
|
+
* putting on a line somebody is scanning for failures.
|
|
279
|
+
*/
|
|
280
|
+
export function mergeEndpoints(artifacts = []) {
|
|
281
|
+
const seen = new Set();
|
|
282
|
+
const endpoints = [];
|
|
283
|
+
// The furthest page reached in each sweep the model made. A turn that lists
|
|
284
|
+
// everything is several sweeps -- one per status -- and every page but the
|
|
285
|
+
// last of each carries has_more, so believing all of them would claim the
|
|
286
|
+
// catalogue was cut short on exactly the turns that read all of it.
|
|
287
|
+
const sweeps = new Map();
|
|
288
|
+
let saved = 0;
|
|
289
|
+
|
|
290
|
+
for (const artifact of artifacts) {
|
|
291
|
+
saved = Math.max(saved, Number(artifact?.total_count) || 0);
|
|
292
|
+
const key = `${artifact?.status ?? ""}|${artifact?.query ?? ""}`;
|
|
293
|
+
const offset = Number(artifact?.offset) || 0;
|
|
294
|
+
const reached = offset + (artifact?.endpoints?.length || 0);
|
|
295
|
+
const furthest = sweeps.get(key);
|
|
296
|
+
if (!furthest || offset >= furthest.offset) {
|
|
297
|
+
sweeps.set(key, {
|
|
298
|
+
offset,
|
|
299
|
+
more: Boolean(artifact?.has_more) || (Number(artifact?.matched_count) || 0) > reached,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
for (const endpoint of artifact?.endpoints || []) {
|
|
303
|
+
const id = String(endpoint?.id ?? "");
|
|
304
|
+
if (id && seen.has(id)) continue;
|
|
305
|
+
if (id) seen.add(id);
|
|
306
|
+
endpoints.push(endpoint);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const routes = new Map();
|
|
311
|
+
const counts = { failing: 0, untested: 0, passing: 0 };
|
|
312
|
+
for (const endpoint of endpoints) {
|
|
313
|
+
const method = String(endpoint?.method || "GET").trim().toUpperCase();
|
|
314
|
+
const path = endpointRoutePath(endpoint);
|
|
315
|
+
const bucket = endpointBucket(endpoint?.last_run);
|
|
316
|
+
counts[bucket] += 1;
|
|
317
|
+
const key = `${method} ${path}`;
|
|
318
|
+
const found = routes.get(key);
|
|
319
|
+
if (!found) {
|
|
320
|
+
routes.set(key, { method, path, bucket, count: 1 });
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
found.count += 1;
|
|
324
|
+
if (BUCKET_ORDER.indexOf(bucket) < BUCKET_ORDER.indexOf(found.bucket)) found.bucket = bucket;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const ordered = [...routes.values()].sort(
|
|
328
|
+
(left, right) => methodRank(left.method) - methodRank(right.method) || left.path.localeCompare(right.path)
|
|
329
|
+
);
|
|
330
|
+
return { endpoints, routes: ordered, counts, saved, more: [...sweeps.values()].some((s) => s.more) };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* The endpoints of a turn, drawn for a terminal.
|
|
335
|
+
*
|
|
336
|
+
* Until now this surface printed the artifact's title and dropped its payload,
|
|
337
|
+
* so a request to list 284 endpoints produced thirteen lines reading "showing
|
|
338
|
+
* 51-75 of 197" and not one endpoint. The model's reply said the list was
|
|
339
|
+
* above, and in the app it is -- the card renders the rows. Here there is no
|
|
340
|
+
* card and no page to send anybody to: the transcript is the whole surface, so
|
|
341
|
+
* the rows have to land in it.
|
|
342
|
+
*
|
|
343
|
+
* Piped output is a different contract and gets a different shape: one line per
|
|
344
|
+
* endpoint, ungrouped and never truncated, because on the other end of a pipe
|
|
345
|
+
* this is something being grepped rather than read.
|
|
346
|
+
*/
|
|
347
|
+
export function endpointsBlock(artifacts, { columns = 80, live = true, paint } = {}) {
|
|
348
|
+
const ink = paint || makePaint(false);
|
|
349
|
+
const { endpoints, routes, counts, saved, more } = mergeEndpoints(artifacts);
|
|
350
|
+
if (!endpoints.length) return "";
|
|
351
|
+
|
|
352
|
+
if (!live) {
|
|
353
|
+
const lines = endpoints.map((endpoint) => {
|
|
354
|
+
const method = String(endpoint?.method || "GET").trim().toUpperCase();
|
|
355
|
+
const where = String(endpoint?.url || "").trim() || endpointRoutePath(endpoint);
|
|
356
|
+
return ` ${endpointBucket(endpoint?.last_run).padEnd(8)} ${method.padEnd(7)} ${where}`;
|
|
357
|
+
});
|
|
358
|
+
return `${lines.join("\n")}\n`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const shown = endpoints.length;
|
|
362
|
+
const head = [`${shown} endpoint${shown === 1 ? "" : "s"}`];
|
|
363
|
+
if (saved > shown) head[0] = `${shown} of ${saved} endpoints`;
|
|
364
|
+
if (routes.length !== shown) head.push(`${routes.length} route${routes.length === 1 ? "" : "s"}`);
|
|
365
|
+
for (const bucket of BUCKET_ORDER) {
|
|
366
|
+
if (counts[bucket]) head.push(`${counts[bucket]} ${bucket}`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const width = Math.max(24, columns);
|
|
370
|
+
// Truncated before it is painted, not after: the colour runs are bytes that
|
|
371
|
+
// `truncate` would count and cut through, and half an escape sequence on a
|
|
372
|
+
// narrow window is a terminal left in the wrong colour.
|
|
373
|
+
const lines = [
|
|
374
|
+
` ${ink.bold("Endpoints")} ${ink.dim(`\u00b7 ${truncate(head.join(" \u00b7 "), Math.max(8, width - 14))}`)}`,
|
|
375
|
+
];
|
|
376
|
+
const method = Math.min(7, Math.max(...routes.map((route) => route.method.length)));
|
|
377
|
+
const room = Math.max(8, width - 6 - method - 2);
|
|
378
|
+
|
|
379
|
+
for (const bucket of BUCKET_ORDER) {
|
|
380
|
+
const group = routes.filter((route) => route.bucket === bucket);
|
|
381
|
+
if (!group.length) continue;
|
|
382
|
+
lines.push("", ` ${ink.bold(BUCKET_LABELS[bucket])} ${ink.dim(`(${counts[bucket]})`)}`);
|
|
383
|
+
for (const route of group) {
|
|
384
|
+
// A hollow mark for untested, because "no run yet" is an absence rather
|
|
385
|
+
// than a result, and a filled dot in any colour reads as one.
|
|
386
|
+
const dot =
|
|
387
|
+
bucket === "failing" ? ink.red("\u25cf") : bucket === "passing" ? ink.green("\u25cf") : ink.dim("\u25cb");
|
|
388
|
+
const also = route.count > 1 ? ink.dim(` (${route.count})`) : "";
|
|
389
|
+
lines.push(` ${dot} ${ink.dim(route.method.padEnd(method))} ${truncate(route.path, room)}${also}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Said once, at the end, rather than thirteen times on the way: the model
|
|
394
|
+
// stopped paging before the catalogue ran out, and the rest is a command
|
|
395
|
+
// away rather than a page in a browser this session cannot open.
|
|
396
|
+
if (more) {
|
|
397
|
+
const rest = `More were not fetched. ${cliInvocation()} endpoints list for the rest.`;
|
|
398
|
+
lines.push("", ` ${ink.dim(truncate(rest, width - 2))}`);
|
|
399
|
+
}
|
|
400
|
+
return `${lines.join("\n")}\n`;
|
|
401
|
+
}
|
|
402
|
+
|
|
220
403
|
/** The permission questions in a finished turn, in the order they were asked. */
|
|
221
404
|
export function permissionQuestions(artifacts = []) {
|
|
222
405
|
return (Array.isArray(artifacts) ? artifacts : []).filter(
|
|
@@ -268,6 +451,13 @@ function makePaint(enabled) {
|
|
|
268
451
|
*/
|
|
269
452
|
export function createRenderer({ stream = process.stdout, paint, live } = {}) {
|
|
270
453
|
const isLive = live ?? Boolean(stream.isTTY);
|
|
454
|
+
// Whether there is a window here, which is a different question from whether
|
|
455
|
+
// it may be coloured. `NO_COLOR=1` is a widespread way of saying "no colour",
|
|
456
|
+
// and it used to reach layout as "this is a pipe" -- so somebody who set it
|
|
457
|
+
// and sat down at a terminal got the shape meant for `grep`. Colour is the
|
|
458
|
+
// palette's business; how wide the window is and whether rows may be grouped
|
|
459
|
+
// and clipped is this.
|
|
460
|
+
const isTerminal = Boolean(stream.isTTY);
|
|
271
461
|
const ink = paint || makePaint(isLive);
|
|
272
462
|
let timer = null;
|
|
273
463
|
let frame = 0;
|
|
@@ -277,6 +467,8 @@ export function createRenderer({ stream = process.stdout, paint, live } = {}) {
|
|
|
277
467
|
let stallTick = 0;
|
|
278
468
|
let wroteReply = false;
|
|
279
469
|
let atLineStart = true;
|
|
470
|
+
// Endpoint pages, held until the turn has something else to say.
|
|
471
|
+
let pages = [];
|
|
280
472
|
|
|
281
473
|
function stopStall() {
|
|
282
474
|
if (!stall) return false;
|
|
@@ -312,6 +504,26 @@ export function createRenderer({ stream = process.stdout, paint, live } = {}) {
|
|
|
312
504
|
stream.write(`${CLEAR_LINE}${tint(`${word}${runOn} ${dots}`)}`);
|
|
313
505
|
}
|
|
314
506
|
|
|
507
|
+
/**
|
|
508
|
+
* Draw the endpoint pages collected so far, as one table.
|
|
509
|
+
*
|
|
510
|
+
* Held rather than printed on arrival because a single "list my endpoints"
|
|
511
|
+
* is thirteen tool calls, and thirteen tables of 25 rows is not a list --
|
|
512
|
+
* it is the pagination read aloud. Flushed by whatever speaks next, so the
|
|
513
|
+
* rows still land *above* the reply that refers to them.
|
|
514
|
+
*/
|
|
515
|
+
function flushPages() {
|
|
516
|
+
if (!pages.length) return;
|
|
517
|
+
const held = pages;
|
|
518
|
+
pages = [];
|
|
519
|
+
const block = endpointsBlock(held, { columns: stream.columns || 80, live: isTerminal, paint: ink });
|
|
520
|
+
if (!block) return;
|
|
521
|
+
clearSpinner();
|
|
522
|
+
if (!atLineStart) stream.write("\n");
|
|
523
|
+
stream.write(block);
|
|
524
|
+
atLineStart = true;
|
|
525
|
+
}
|
|
526
|
+
|
|
315
527
|
function draw() {
|
|
316
528
|
if (!isLive || !label) return;
|
|
317
529
|
const spin = FRAMES[frame % FRAMES.length];
|
|
@@ -347,8 +559,16 @@ export function createRenderer({ stream = process.stdout, paint, live } = {}) {
|
|
|
347
559
|
}
|
|
348
560
|
},
|
|
349
561
|
artifact(item) {
|
|
562
|
+
// Endpoint pages are the one artifact whose payload is the point, and
|
|
563
|
+
// they arrive one page at a time, so they are collected rather than
|
|
564
|
+
// drawn. Everything else is a card this surface summarises in a line.
|
|
565
|
+
if (item && item.type === "endpoints" && Array.isArray(item.endpoints)) {
|
|
566
|
+
pages.push(item);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
350
569
|
const line = artifactLine(item);
|
|
351
570
|
if (!line) return;
|
|
571
|
+
flushPages();
|
|
352
572
|
clearSpinner();
|
|
353
573
|
if (!atLineStart) stream.write("\n");
|
|
354
574
|
stream.write(` ${ink.cyan("●")} ${line}\n`);
|
|
@@ -357,6 +577,7 @@ export function createRenderer({ stream = process.stdout, paint, live } = {}) {
|
|
|
357
577
|
delta(text) {
|
|
358
578
|
const chunk = String(text ?? "");
|
|
359
579
|
if (!chunk) return;
|
|
580
|
+
flushPages();
|
|
360
581
|
clearSpinner();
|
|
361
582
|
if (!wroteReply) {
|
|
362
583
|
stream.write("\n");
|
|
@@ -373,18 +594,23 @@ export function createRenderer({ stream = process.stdout, paint, live } = {}) {
|
|
|
373
594
|
this.delta(body);
|
|
374
595
|
},
|
|
375
596
|
note(text) {
|
|
597
|
+
flushPages();
|
|
376
598
|
clearSpinner();
|
|
377
599
|
if (!atLineStart) stream.write("\n");
|
|
378
600
|
stream.write(`${ink.dim(String(text))}\n`);
|
|
379
601
|
atLineStart = true;
|
|
380
602
|
},
|
|
381
603
|
error(text) {
|
|
604
|
+
flushPages();
|
|
382
605
|
clearSpinner();
|
|
383
606
|
if (!atLineStart) stream.write("\n");
|
|
384
607
|
stream.write(`${ink.red(String(text))}\n`);
|
|
385
608
|
atLineStart = true;
|
|
386
609
|
},
|
|
387
610
|
end() {
|
|
611
|
+
// The backstop: a turn that listed endpoints and then said nothing at
|
|
612
|
+
// all still has to put them on the screen.
|
|
613
|
+
flushPages();
|
|
388
614
|
clearSpinner();
|
|
389
615
|
if (!atLineStart) stream.write("\n");
|
|
390
616
|
if (wroteReply) stream.write("\n");
|
|
@@ -705,7 +931,14 @@ export function holdTerminal({ stream = process.stdout, footer = () => "" } = {}
|
|
|
705
931
|
if (restored) return;
|
|
706
932
|
restored = true;
|
|
707
933
|
for (const fn of teardown.splice(0)) safely(fn);
|
|
708
|
-
|
|
934
|
+
// No `ESC[?1049l` here any more, and its absence is load-bearing rather
|
|
935
|
+
// than tidying. The session never enters the alternate screen, so leaving
|
|
936
|
+
// it would be addressed to a buffer nobody is on -- and the sequence is
|
|
937
|
+
// not inert: it restores the cursor as DECRC does, from the same saved
|
|
938
|
+
// slot the dock writes the transcript's position into with `ESC7`. Sent on
|
|
939
|
+
// the shell's own screen it would drag the cursor back up into the
|
|
940
|
+
// transcript and print the resume footer over the conversation.
|
|
941
|
+
if (live) safely(() => stream.write(`${RELEASE_REGION}${SHOW_CURSOR}${CLEAR_TITLE}`));
|
|
709
942
|
safely(() => {
|
|
710
943
|
const text = footer();
|
|
711
944
|
if (text) stream.write(text);
|
|
@@ -1236,7 +1469,32 @@ export function createDock({
|
|
|
1236
1469
|
at = null;
|
|
1237
1470
|
const { rows } = size();
|
|
1238
1471
|
stream.write("\u001b[r");
|
|
1239
|
-
|
|
1472
|
+
// Every row the furniture owned, not just the last one. On the alternate
|
|
1473
|
+
// screen clearing one row was enough because leaving the buffer threw the
|
|
1474
|
+
// whole screen away; on the shell's own screen the chrome is real output,
|
|
1475
|
+
// and a composer box and a status strip left sitting under the final
|
|
1476
|
+
// prompt are the last thing the session shows.
|
|
1477
|
+
//
|
|
1478
|
+
// Guarded on having painted, because that is the only thing that says
|
|
1479
|
+
// these rows were ever the dock's. In a window too short to dock in
|
|
1480
|
+
// nothing is drawn at all -- the session is a plain prompt -- and erasing
|
|
1481
|
+
// four rows of a five-row window there would take somebody's transcript
|
|
1482
|
+
// with it. Clamped to the live window for the same reason `resize` is:
|
|
1483
|
+
// after a shrink, `painted.rows` names rows that no longer exist.
|
|
1484
|
+
if (painted) {
|
|
1485
|
+
const bottom = Math.min(painted.rows, rows);
|
|
1486
|
+
const top = Math.max(1, bottom - DOCK_ROWS + 1);
|
|
1487
|
+
for (let row = top; row <= bottom; row += 1) {
|
|
1488
|
+
stream.write(`\u001b[${row};1H\u001b[2K`);
|
|
1489
|
+
}
|
|
1490
|
+
// Carry on immediately below the transcript rather than at the bottom
|
|
1491
|
+
// of the window, so the resume footer and the shell prompt follow the
|
|
1492
|
+
// conversation instead of a gap where the dock used to be.
|
|
1493
|
+
stream.write(`\u001b[${top};1H`);
|
|
1494
|
+
} else {
|
|
1495
|
+
stream.write(`\u001b[${rows};1H\u001b[2K`);
|
|
1496
|
+
}
|
|
1497
|
+
painted = null;
|
|
1240
1498
|
stream.write(CLEAR_TITLE);
|
|
1241
1499
|
},
|
|
1242
1500
|
};
|
|
@@ -1738,10 +1996,27 @@ export async function agentCommand(commandArgs = [], { authenticate = authentica
|
|
|
1738
1996
|
if (args.has("--print") || args.has("-p") || !process.stdin.isTTY) return;
|
|
1739
1997
|
}
|
|
1740
1998
|
|
|
1741
|
-
// The session's
|
|
1742
|
-
//
|
|
1743
|
-
//
|
|
1744
|
-
|
|
1999
|
+
// The session shares the shell's screen rather than taking one of its own.
|
|
2000
|
+
//
|
|
2001
|
+
// It used to enter the alternate screen, the way a full-screen app does, and
|
|
2002
|
+
// that cost the one thing a chat transcript needs: the alternate buffer keeps
|
|
2003
|
+
// no history, so a reply that ran past the top of the window was not scrolled
|
|
2004
|
+
// away, it was discarded. There was nothing to scroll back to in any
|
|
2005
|
+
// terminal, because the buffer holds exactly one screen.
|
|
2006
|
+
//
|
|
2007
|
+
// On the shell's own screen the transcript is ordinary output, so the wheel,
|
|
2008
|
+
// Page Up, find and select-to-copy all work on a conversation the way they
|
|
2009
|
+
// work on any other command's output. Keeping the composer docked costs none
|
|
2010
|
+
// of it: the dock's scrolling region starts at row 1, and a region whose top
|
|
2011
|
+
// margin is the first row is the case where a terminal pushes the lines it
|
|
2012
|
+
// scrolls past into the scrollback instead of dropping them.
|
|
2013
|
+
//
|
|
2014
|
+
// What this gives up is what the alternate screen was for -- a new session
|
|
2015
|
+
// can now be scrolled back into a finished one. That is the trade every
|
|
2016
|
+
// other command makes with its own output, and it is the cheaper way round:
|
|
2017
|
+
// scrolling into an old session is something a reader can see and scroll out
|
|
2018
|
+
// of, and a transcript that cannot be scrolled at all is not.
|
|
2019
|
+
const interactive = Boolean(process.stdout.isTTY);
|
|
1745
2020
|
|
|
1746
2021
|
// Watching before the screen is taken, not after: the handlers have to be up
|
|
1747
2022
|
// for every moment the terminal is in a state somebody else has to live with.
|
|
@@ -1755,13 +2030,11 @@ export async function agentCommand(commandArgs = [], { authenticate = authentica
|
|
|
1755
2030
|
// A conversation whose id never arrived stringifies to "undefined", and a
|
|
1756
2031
|
// resume line naming that is worse than no resume line.
|
|
1757
2032
|
const id = String(conversation?.id || "");
|
|
1758
|
-
if (!
|
|
2033
|
+
if (!interactive || !id || id === "undefined") return "";
|
|
1759
2034
|
return `${paint.dim("Resume this session with:")}\n ${cliInvocation()} --conversation ${id}\n`;
|
|
1760
2035
|
},
|
|
1761
2036
|
}).watch();
|
|
1762
2037
|
|
|
1763
|
-
if (fullScreen) process.stdout.write(`${ENTER_ALT}${CLEAR_SCREEN}`);
|
|
1764
|
-
|
|
1765
2038
|
const dock = createDock({
|
|
1766
2039
|
stream: process.stdout,
|
|
1767
2040
|
paint,
|
|
@@ -1770,13 +2043,13 @@ export async function agentCommand(commandArgs = [], { authenticate = authentica
|
|
|
1770
2043
|
const prompt = `${paint.caret("\u276f")} `;
|
|
1771
2044
|
const rl = createInterface({ input: process.stdin, output: process.stdout, prompt });
|
|
1772
2045
|
dock.attach(rl);
|
|
1773
|
-
//
|
|
1774
|
-
//
|
|
1775
|
-
//
|
|
1776
|
-
//
|
|
1777
|
-
//
|
|
1778
|
-
//
|
|
1779
|
-
dock.enable(
|
|
2046
|
+
// No start row, because there is no cleared screen to start at the top of any
|
|
2047
|
+
// more. Told nothing, the dock makes room by scrolling the shell up by its
|
|
2048
|
+
// own height and carries on at the floor -- which keeps whatever was on the
|
|
2049
|
+
// screen before, the prompt the session was launched from included, instead
|
|
2050
|
+
// of erasing it. The banner still goes through the dock rather than around
|
|
2051
|
+
// it, which is what keeps the two in step.
|
|
2052
|
+
dock.enable();
|
|
1780
2053
|
|
|
1781
2054
|
dock.write(banner({ paint, workspace, backend: backendUrl(args), conversation }));
|
|
1782
2055
|
const onResize = () => dock.resize();
|