drexler 0.2.14 → 0.2.15
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/README.md +7 -1
- package/package.json +1 -1
- package/src/commands.ts +127 -20
- package/src/config.ts +141 -32
- package/src/conversation.ts +0 -4
- package/src/index.ts +68 -5
- package/src/pet/petState.ts +408 -0
- package/src/repl.ts +1 -1
- package/src/ui/App.tsx +543 -144
- package/src/ui/CommandPalette.tsx +2 -0
- package/src/ui/DealDeskHeader.tsx +0 -5
- package/src/ui/DeathScreen.tsx +110 -0
- package/src/ui/MarkdownBody.tsx +29 -5
- package/src/ui/MascotIntro.tsx +158 -57
- package/src/ui/Message.tsx +2 -105
- package/src/ui/PetPanel.tsx +537 -0
- package/src/ui/TranscriptViewport.tsx +206 -48
- package/src/ui/displayContent.ts +5 -2
|
@@ -27,13 +27,29 @@ export interface TranscriptViewportProps {
|
|
|
27
27
|
|
|
28
28
|
interface TranscriptEntry {
|
|
29
29
|
key: string;
|
|
30
|
-
node:
|
|
30
|
+
node:
|
|
31
|
+
| ReactNode
|
|
32
|
+
| ((clipTo?: { readonly start: number; readonly rows: number }) => ReactNode);
|
|
31
33
|
estimatedRows: number;
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
interface VisibleEntry {
|
|
37
|
+
entry: TranscriptEntry;
|
|
38
|
+
clipStart?: number;
|
|
39
|
+
clipRows?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
const DEFAULT_MAX_ROWS = 18;
|
|
35
43
|
const DEFAULT_COLS = 80;
|
|
36
44
|
const MIN_COLS = 1;
|
|
45
|
+
const HEADER_FOOTER_ROWS = 2;
|
|
46
|
+
const TRUNCATION_HINT_ROWS = 1;
|
|
47
|
+
const MIN_TRUNCATED_BODY_ROWS = 1;
|
|
48
|
+
|
|
49
|
+
function truncationHint(dropped: number, direction: "earlier" | "newer"): string {
|
|
50
|
+
const keyHint = direction === "earlier" ? "PageUp scrollback" : "PageDown newer";
|
|
51
|
+
return `... ${dropped} line${dropped === 1 ? "" : "s"} ${direction} — ${keyHint} to read`;
|
|
52
|
+
}
|
|
37
53
|
|
|
38
54
|
const ROLE_LABELS: Record<TranscriptViewportItem["role"], string> = {
|
|
39
55
|
user: "YOU",
|
|
@@ -202,8 +218,15 @@ function displayContentForItem(item: TranscriptViewportItem): string {
|
|
|
202
218
|
}
|
|
203
219
|
|
|
204
220
|
function displayLinesForItem(item: TranscriptViewportItem): AssistantDisplayLine[] {
|
|
205
|
-
|
|
206
|
-
|
|
221
|
+
const lines =
|
|
222
|
+
item.role === "assistant"
|
|
223
|
+
? assistantDisplayLines(item.content)
|
|
224
|
+
: item.content.split("\n").map((text) => ({ kind: "text" as const, text }));
|
|
225
|
+
let start = 0;
|
|
226
|
+
let end = lines.length;
|
|
227
|
+
while (start < end && lines[start]?.text.trim().length === 0) start += 1;
|
|
228
|
+
while (end > start && lines[end - 1]?.text.trim().length === 0) end -= 1;
|
|
229
|
+
return lines.slice(start, end);
|
|
207
230
|
}
|
|
208
231
|
|
|
209
232
|
function wrappedTranscriptLines(
|
|
@@ -291,6 +314,14 @@ function tokenizeCodeLine(line: string): CodeToken[] {
|
|
|
291
314
|
return tokens;
|
|
292
315
|
}
|
|
293
316
|
|
|
317
|
+
export function estimateTranscriptRows(
|
|
318
|
+
items: readonly TranscriptViewportItem[],
|
|
319
|
+
compact: boolean,
|
|
320
|
+
cols: number,
|
|
321
|
+
): number {
|
|
322
|
+
return items.reduce((sum, item) => sum + itemRows(item, compact, cols), 0);
|
|
323
|
+
}
|
|
324
|
+
|
|
294
325
|
function itemRows(
|
|
295
326
|
item: TranscriptViewportItem,
|
|
296
327
|
compact: boolean,
|
|
@@ -326,10 +357,14 @@ function DefaultTranscriptItem({
|
|
|
326
357
|
item,
|
|
327
358
|
compact,
|
|
328
359
|
cols,
|
|
360
|
+
clipStart = 0,
|
|
361
|
+
maxRows,
|
|
329
362
|
}: {
|
|
330
363
|
item: TranscriptViewportItem;
|
|
331
364
|
compact: boolean;
|
|
332
365
|
cols: number;
|
|
366
|
+
clipStart?: number;
|
|
367
|
+
maxRows?: number;
|
|
333
368
|
}) {
|
|
334
369
|
const t = useTheme();
|
|
335
370
|
const label = ROLE_LABELS[item.role];
|
|
@@ -362,7 +397,7 @@ function DefaultTranscriptItem({
|
|
|
362
397
|
);
|
|
363
398
|
});
|
|
364
399
|
|
|
365
|
-
if (compact) {
|
|
400
|
+
if (compact || (maxRows !== undefined && maxRows < HEADER_FOOTER_ROWS + 1)) {
|
|
366
401
|
const marker = item.role === "assistant" ? "◆" : ROLE_MARKERS[item.role];
|
|
367
402
|
const prefix = `${label} ${marker} `;
|
|
368
403
|
const budget = Math.max(1, cols - displayWidth(prefix));
|
|
@@ -386,7 +421,47 @@ function DefaultTranscriptItem({
|
|
|
386
421
|
const footerWidth = Math.max(0, cols - 2);
|
|
387
422
|
const bodyPrefix = bodyPrefixForRole(item.role);
|
|
388
423
|
const contentWidth = transcriptContentWidth(item.role, cols);
|
|
389
|
-
const
|
|
424
|
+
const allDisplayLines = wrappedTranscriptLines(item, contentWidth);
|
|
425
|
+
let displayLines: WrappedTranscriptLine[] = allDisplayLines;
|
|
426
|
+
if (maxRows !== undefined) {
|
|
427
|
+
const bodyBudget = Math.max(
|
|
428
|
+
MIN_TRUNCATED_BODY_ROWS,
|
|
429
|
+
maxRows - HEADER_FOOTER_ROWS,
|
|
430
|
+
);
|
|
431
|
+
const bodyStart = Math.max(0, Math.min(allDisplayLines.length, clipStart - 1));
|
|
432
|
+
const before = bodyStart;
|
|
433
|
+
const afterAvailable = Math.max(0, allDisplayLines.length - bodyStart);
|
|
434
|
+
const needsTopHint = before > 0;
|
|
435
|
+
const needsBottomHint = afterAvailable > bodyBudget;
|
|
436
|
+
const hintRows =
|
|
437
|
+
(needsTopHint ? TRUNCATION_HINT_ROWS : 0) +
|
|
438
|
+
(needsBottomHint ? TRUNCATION_HINT_ROWS : 0);
|
|
439
|
+
const keep = Math.max(0, bodyBudget - hintRows);
|
|
440
|
+
const bodyEnd = Math.min(allDisplayLines.length, bodyStart + keep);
|
|
441
|
+
if (needsTopHint || bodyEnd < allDisplayLines.length) {
|
|
442
|
+
const droppedBefore = before;
|
|
443
|
+
const droppedAfter = allDisplayLines.length - bodyEnd;
|
|
444
|
+
displayLines = [
|
|
445
|
+
...(droppedBefore > 0
|
|
446
|
+
? [
|
|
447
|
+
{
|
|
448
|
+
kind: "text" as const,
|
|
449
|
+
text: truncationHint(droppedBefore, "earlier"),
|
|
450
|
+
},
|
|
451
|
+
]
|
|
452
|
+
: []),
|
|
453
|
+
...allDisplayLines.slice(bodyStart, bodyEnd),
|
|
454
|
+
...(droppedAfter > 0
|
|
455
|
+
? [
|
|
456
|
+
{
|
|
457
|
+
kind: "text" as const,
|
|
458
|
+
text: truncationHint(droppedAfter, "newer"),
|
|
459
|
+
},
|
|
460
|
+
]
|
|
461
|
+
: []),
|
|
462
|
+
];
|
|
463
|
+
}
|
|
464
|
+
}
|
|
390
465
|
|
|
391
466
|
return (
|
|
392
467
|
<Box flexDirection="column" width={cols} flexShrink={1}>
|
|
@@ -460,11 +535,17 @@ function itemsToEntries({
|
|
|
460
535
|
}): TranscriptEntry[] {
|
|
461
536
|
return items.map((item, index) => ({
|
|
462
537
|
key: String(item.id ?? index),
|
|
463
|
-
node: renderItem
|
|
464
|
-
renderItem(item, index)
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
538
|
+
node: renderItem
|
|
539
|
+
? renderItem(item, index)
|
|
540
|
+
: (clipTo?: { readonly start: number; readonly rows: number }) => (
|
|
541
|
+
<DefaultTranscriptItem
|
|
542
|
+
item={item}
|
|
543
|
+
compact={compact}
|
|
544
|
+
cols={cols}
|
|
545
|
+
clipStart={clipTo?.start}
|
|
546
|
+
maxRows={clipTo?.rows}
|
|
547
|
+
/>
|
|
548
|
+
),
|
|
468
549
|
estimatedRows: itemRows(item, compact, cols),
|
|
469
550
|
}));
|
|
470
551
|
}
|
|
@@ -474,71 +555,134 @@ function selectWindow(
|
|
|
474
555
|
maxRows: number,
|
|
475
556
|
scrollOffset: number,
|
|
476
557
|
): {
|
|
477
|
-
visible:
|
|
558
|
+
visible: VisibleEntry[];
|
|
478
559
|
hiddenBefore: number;
|
|
479
560
|
hiddenAfter: number;
|
|
561
|
+
hiddenRowsBefore: number;
|
|
562
|
+
hiddenRowsAfter: number;
|
|
480
563
|
} {
|
|
481
564
|
if (entries.length === 0) {
|
|
482
|
-
return {
|
|
565
|
+
return {
|
|
566
|
+
visible: [],
|
|
567
|
+
hiddenBefore: 0,
|
|
568
|
+
hiddenAfter: 0,
|
|
569
|
+
hiddenRowsBefore: 0,
|
|
570
|
+
hiddenRowsAfter: 0,
|
|
571
|
+
};
|
|
483
572
|
}
|
|
484
573
|
|
|
485
574
|
const safeRows = Math.max(1, Math.floor(maxRows));
|
|
486
|
-
const
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
const
|
|
490
|
-
|
|
575
|
+
const rowCounts = entries.map((entry) => Math.max(1, entry.estimatedRows));
|
|
576
|
+
const totalRows = rowCounts.reduce((sum, rows) => sum + rows, 0);
|
|
577
|
+
const maxOffset = Math.max(0, totalRows - 1);
|
|
578
|
+
const safeOffset = Math.max(0, Math.min(Math.floor(scrollOffset), maxOffset));
|
|
579
|
+
const endRow = Math.max(1, totalRows - safeOffset);
|
|
580
|
+
let indicatorRows = 0;
|
|
581
|
+
let startRow = Math.max(0, endRow - safeRows);
|
|
491
582
|
|
|
492
583
|
for (let pass = 0; pass < 3; pass++) {
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
if (used >= budget) break;
|
|
504
|
-
}
|
|
584
|
+
const hiddenBeforeRows = startRow;
|
|
585
|
+
const hiddenAfterRows = Math.max(0, totalRows - endRow);
|
|
586
|
+
const nextIndicatorRows = Math.min(
|
|
587
|
+
safeRows - 1,
|
|
588
|
+
(hiddenBeforeRows > 0 ? 1 : 0) + (hiddenAfterRows > 0 ? 1 : 0),
|
|
589
|
+
);
|
|
590
|
+
if (nextIndicatorRows === indicatorRows) break;
|
|
591
|
+
indicatorRows = nextIndicatorRows;
|
|
592
|
+
startRow = Math.max(0, endRow - Math.max(1, safeRows - indicatorRows));
|
|
593
|
+
}
|
|
505
594
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
595
|
+
const buildVisible = (fromRow: number, toRow: number): VisibleEntry[] => {
|
|
596
|
+
const next: VisibleEntry[] = [];
|
|
597
|
+
let rowCursor = 0;
|
|
598
|
+
for (let i = 0; i < entries.length; i++) {
|
|
599
|
+
const entry = entries[i]!;
|
|
600
|
+
const rows = rowCounts[i]!;
|
|
601
|
+
const entryStart = rowCursor;
|
|
602
|
+
const entryEnd = rowCursor + rows;
|
|
603
|
+
rowCursor = entryEnd;
|
|
604
|
+
const overlapStart = Math.max(fromRow, entryStart);
|
|
605
|
+
const overlapEnd = Math.min(toRow, entryEnd);
|
|
606
|
+
if (overlapStart >= overlapEnd) continue;
|
|
607
|
+
next.push({
|
|
608
|
+
entry,
|
|
609
|
+
clipStart: overlapStart - entryStart,
|
|
610
|
+
clipRows: overlapEnd - overlapStart,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
return next;
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
let visible = buildVisible(startRow, endRow);
|
|
617
|
+
if (
|
|
618
|
+
indicatorRows > 0 &&
|
|
619
|
+
visible.some(
|
|
620
|
+
({ entry, clipRows }) =>
|
|
621
|
+
entry.estimatedRows >= HEADER_FOOTER_ROWS + MIN_TRUNCATED_BODY_ROWS &&
|
|
622
|
+
(clipRows ?? entry.estimatedRows) <
|
|
623
|
+
HEADER_FOOTER_ROWS + MIN_TRUNCATED_BODY_ROWS,
|
|
624
|
+
)
|
|
625
|
+
) {
|
|
626
|
+
indicatorRows = 0;
|
|
627
|
+
startRow = Math.max(0, endRow - safeRows);
|
|
628
|
+
visible = buildVisible(startRow, endRow);
|
|
509
629
|
}
|
|
510
630
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
if (
|
|
520
|
-
if (
|
|
631
|
+
let hiddenBefore = 0;
|
|
632
|
+
let hiddenAfter = 0;
|
|
633
|
+
let cursor = 0;
|
|
634
|
+
for (let i = 0; i < entries.length; i++) {
|
|
635
|
+
const rows = rowCounts[i]!;
|
|
636
|
+
const entryStart = cursor;
|
|
637
|
+
const entryEnd = cursor + rows;
|
|
638
|
+
cursor = entryEnd;
|
|
639
|
+
if (entryEnd <= startRow) hiddenBefore += 1;
|
|
640
|
+
else if (entryStart < startRow && entryEnd > startRow) hiddenBefore += 1;
|
|
641
|
+
if (entryStart >= endRow) hiddenAfter += 1;
|
|
642
|
+
else if (entryStart < endRow && entryEnd > endRow) hiddenAfter += 1;
|
|
643
|
+
}
|
|
644
|
+
let hiddenRowsBefore = startRow;
|
|
645
|
+
let hiddenRowsAfter = Math.max(0, totalRows - endRow);
|
|
646
|
+
const showTopIndicator = indicatorRows > 0 && hiddenRowsBefore > 0;
|
|
647
|
+
const showBottomIndicator =
|
|
648
|
+
indicatorRows > (showTopIndicator ? 1 : 0) && hiddenRowsAfter > 0;
|
|
649
|
+
if (!showTopIndicator) {
|
|
650
|
+
hiddenBefore = 0;
|
|
651
|
+
hiddenRowsBefore = 0;
|
|
652
|
+
}
|
|
653
|
+
if (!showBottomIndicator) {
|
|
654
|
+
hiddenAfter = 0;
|
|
655
|
+
hiddenRowsAfter = 0;
|
|
521
656
|
}
|
|
522
657
|
|
|
523
|
-
return {
|
|
658
|
+
return {
|
|
659
|
+
visible,
|
|
660
|
+
hiddenBefore,
|
|
661
|
+
hiddenAfter,
|
|
662
|
+
hiddenRowsBefore,
|
|
663
|
+
hiddenRowsAfter,
|
|
664
|
+
};
|
|
524
665
|
}
|
|
525
666
|
|
|
526
667
|
function ScrollIndicator({
|
|
527
668
|
direction,
|
|
528
669
|
count,
|
|
670
|
+
rows,
|
|
529
671
|
compact,
|
|
530
672
|
cols,
|
|
531
673
|
}: {
|
|
532
674
|
direction: "earlier" | "newer";
|
|
533
675
|
count: number;
|
|
676
|
+
rows: number;
|
|
534
677
|
compact: boolean;
|
|
535
678
|
cols: number;
|
|
536
679
|
}) {
|
|
537
680
|
const t = useTheme();
|
|
538
681
|
const arrow = direction === "earlier" ? "↑" : "↓";
|
|
682
|
+
const keyHint = direction === "earlier" ? "PageUp scrollback" : "PageDown newer";
|
|
539
683
|
const label = compact
|
|
540
|
-
? `${arrow} ${
|
|
541
|
-
: `${arrow} ${
|
|
684
|
+
? `${arrow} ${rows} ${direction}`
|
|
685
|
+
: `${arrow} ${rows} line${rows === 1 ? "" : "s"} ${direction} (${count} item${count === 1 ? "" : "s"} hidden) — ${keyHint}`;
|
|
542
686
|
|
|
543
687
|
return (
|
|
544
688
|
<Box width={cols} flexShrink={1}>
|
|
@@ -566,7 +710,13 @@ function TranscriptViewportInner({
|
|
|
566
710
|
: childrenToEntries(children),
|
|
567
711
|
[children, compact, items, renderItem, width],
|
|
568
712
|
);
|
|
569
|
-
const {
|
|
713
|
+
const {
|
|
714
|
+
visible,
|
|
715
|
+
hiddenBefore,
|
|
716
|
+
hiddenAfter,
|
|
717
|
+
hiddenRowsBefore,
|
|
718
|
+
hiddenRowsAfter,
|
|
719
|
+
} = useMemo(
|
|
570
720
|
() => selectWindow(entries, maxRows, scrollOffset),
|
|
571
721
|
[entries, maxRows, scrollOffset],
|
|
572
722
|
);
|
|
@@ -577,19 +727,27 @@ function TranscriptViewportInner({
|
|
|
577
727
|
<ScrollIndicator
|
|
578
728
|
direction="earlier"
|
|
579
729
|
count={hiddenBefore}
|
|
730
|
+
rows={hiddenRowsBefore}
|
|
580
731
|
compact={compact}
|
|
581
732
|
cols={width}
|
|
582
733
|
/>
|
|
583
734
|
) : null}
|
|
584
|
-
{visible.map((entry) => (
|
|
735
|
+
{visible.map(({ entry, clipStart, clipRows }) => (
|
|
585
736
|
<Box key={entry.key} flexDirection="column" width={width} flexShrink={1}>
|
|
586
|
-
{entry.node
|
|
737
|
+
{typeof entry.node === "function"
|
|
738
|
+
? entry.node(
|
|
739
|
+
clipRows === undefined
|
|
740
|
+
? undefined
|
|
741
|
+
: { start: clipStart ?? 0, rows: clipRows },
|
|
742
|
+
)
|
|
743
|
+
: entry.node}
|
|
587
744
|
</Box>
|
|
588
745
|
))}
|
|
589
746
|
{hiddenAfter > 0 ? (
|
|
590
747
|
<ScrollIndicator
|
|
591
748
|
direction="newer"
|
|
592
749
|
count={hiddenAfter}
|
|
750
|
+
rows={hiddenRowsAfter}
|
|
593
751
|
compact={compact}
|
|
594
752
|
cols={width}
|
|
595
753
|
/>
|
package/src/ui/displayContent.ts
CHANGED
|
@@ -76,15 +76,17 @@ export function normalizeAssistantMarkdownRenderContent(content: string): string
|
|
|
76
76
|
const output: string[] = [];
|
|
77
77
|
let fenceChar = "";
|
|
78
78
|
let fenceLength = 0;
|
|
79
|
+
let fenceMarker = "";
|
|
79
80
|
let markdownFence = false;
|
|
80
81
|
|
|
81
82
|
for (const rawLine of lines) {
|
|
82
83
|
const line = rawLine.replace(/\t/gu, TAB_DISPLAY);
|
|
83
84
|
if (fenceChar.length > 0) {
|
|
84
85
|
if (isFenceClose(line, fenceChar, fenceLength)) {
|
|
85
|
-
if (!markdownFence) output.push(
|
|
86
|
+
if (!markdownFence) output.push(fenceMarker);
|
|
86
87
|
fenceChar = "";
|
|
87
88
|
fenceLength = 0;
|
|
89
|
+
fenceMarker = "";
|
|
88
90
|
markdownFence = false;
|
|
89
91
|
} else {
|
|
90
92
|
output.push(line);
|
|
@@ -98,8 +100,9 @@ export function normalizeAssistantMarkdownRenderContent(content: string): string
|
|
|
98
100
|
const info = openingFence[2] ?? "";
|
|
99
101
|
fenceChar = marker[0]!;
|
|
100
102
|
fenceLength = marker.length;
|
|
103
|
+
fenceMarker = marker;
|
|
101
104
|
markdownFence = isMarkdownFence(info);
|
|
102
|
-
if (!markdownFence) output.push(
|
|
105
|
+
if (!markdownFence) output.push(marker);
|
|
103
106
|
continue;
|
|
104
107
|
}
|
|
105
108
|
|